尽管特定的大脑区域负责某项独立的功能,但这些区域组成的网络以及它们之间的连接才是人类表现出的整体行为的原因
人工智能中的联结主义(connectionist): 通过模拟人类大脑中神经元的网络来处理信号,信号通过类似于 神经元间的连接方式从一个节点传递到另一个节点
细胞体:包含维持神经元新陈代谢的细胞器
树突(Dendrite):
轴突(Axon):
树突和轴突的形态关系:
突触并非随机地、均匀地分布在树突的各个部位,而是遵循特定的模式和规律。
神经元是神经系统传递信息的功能单位,通过对信息的接收和加工,并将其传递给其他神经元,构成了局部或长程的神经回路(circuit)。
- ### 神经回路的复杂性
- 感觉信息可上行至大脑皮质多个区域
- 如初级躯体感觉皮质区和初级运动皮质
- 脊髓反射与大脑控制共同作用

- **顶叶中的躯体感觉区**:
- 接受来自丘脑的躯体感觉输入
- 处理触觉、痛觉、温度和本体感觉
- **枕叶中的视觉加工区**:
- 初级视觉皮质(V1区)位于大脑半球内侧
- 处理颜色、亮度、空间频率、朝向及运动等信息
- **颞叶中的听觉加工区**:
- 听觉信号经丘脑内侧膝状体到达颞叶上部听觉皮质
- **感觉和运动皮质的拓扑图**:
- 身体和大脑皮质之间存在空间拓扑关系
边缘系统(limbic system)也称边缘叶,包括以下区域:扣带回(cingulate gyrus),海马(hippocampus),海马旁回(parahippocampalgyrus),丘脑(thalamus),下丘脑(hypothalamus),杏仁核(amygdala)。
- 视觉:视网膜神经元→外侧膝状体→初级视皮质。听觉:内耳听觉神经元→内侧膝状体→初级。听皮质躯体感觉:躯体感觉:神经元→腹后侧核团→初级躯体感觉皮质
- 丘脑不仅是感觉信息输入大脑的中继站,还接收大量来自**相同皮质区域的输入信息皮质的关口**
- 除嗅觉外,其余感觉信息需经丘脑到达初级感觉皮质
- 是感觉信息中继站,也接收来自皮质的反馈信息
下丘脑(Hypothalamus):
杏仁核(Amygdala):参与情绪处理


人类语言的生成能力:
半球协作与分工模式:
科学认识大脑半球差异:


C 语言是一种通用的、面向过程式的计算机程序设计语言。
1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。

常见的编译器msvc、clang、gcc
GCC(GNU Compiler Collection)
Clang
MSVC(Microsoft Visual C++)
Intel C++ Compiler
集成开发环境 如:VS2022、XCode、CodeBlocks、DevC++、Clion
+ 字符A~Z的ASCII码值从65~90
+ 字符a~z的ASCII码值从97~122
+ 对应的⼤⼩写字符(a和A)的ASCII码值的差值是**32**
+ 数字字符0~9的ASCII码值从48~57
+ 换⾏ \n 的ASCII值是:10
+ 在这些字符中ASCII码值从0~31 这32个字符是不可打印字符,⽆法打印在屏幕上观察
C语⾔是⼀⻔编译型计算机语⾔,C语⾔源代码都是文本文件,⽂本⽂件本⾝⽆法执⾏,必须通过编译器翻译和链接器的链接,⽣成⼆进制的可执行文件,可执⾏⽂件才能执⾏。
C语⾔代码是放在.c为后缀的⽂件中的,要得到最终运⾏的可执⾏程序,中间要经过编译和链接2个过程。
注:
1. 每个源⽂件(.c)单独经过编译器处理⽣成对应的⽬标⽂件(.obj为后缀的⽂件)
2. 多个⽬标⽂件和库⽂件经过链接器处理⽣成对应的可执⾏程序(.exe⽂件)
如果不借助IDE,如何把源代码保存在一个文件中,以及如何编译并运行它。下面是简单的步骤:
#include <stdio.h>
int main()
{
printf("hello world"\n);
return 0;
}
C 程序主要包括以下部分:
所有的C语言程序都需要包含 main() 函数。 代码从 main() 函数开始执行.不可以随意更改主函数的名称,即main,main函数有且只有一个.
//标准主函数的写法
int main()
{
printf("hello world"\n);
return 0;
}
要点:
int main(void)
{
printf("hello world"\n);
return 0;
}
细节:
#include <stdio.h>
std代表standard,i和o指数字输入和输出;.h是头文件(函数的声明,类型的声明,头文件的包含),因为这些文件都是放在程序各文件的开头;.c是源文件(函数的实现).其他:
当编译器遇到 printf() 函数时,如果没有找到 stdio.h 头文件,会发生编译错误。如下:
warning: implicitly declaring library function 'printf' with type'int (const char *, ...)' [-Wimplicit-function-declaration]
int printf(const char *, ...)在 C 语言中,数据类型指的是用于声明不同类型的变量或函数的一个广泛的系统。变量的类型决定了变量存储占用的空间,以及如何解释存储的位模式。
C 中的类型可分为以下几种:

char字符数据类型
字符型(char)用于储存字符(character),如英文字母或标点。但是char类型在内存中并不是以字符的形式储存,而是以ASCII码的形式储存,也可以说char类型储存的实际上是整数。所以char类型也被归类为整形家族。
int main()
{
char c = 'A';
printf("%d\n", c);//会输出'65'
printf("%c\n", c);//会输出A
return 0;
}
既然知道char实际上是整形,所以也可以用int类型对char类型赋值
int main()
{
int c = 65;//这里也可以是char c = 65;
printf("%d\n", c);
printf("%c\n", c);
return 0;
}
结果完全一致,都是
65
A
当听到char的类型这句话时,第一反应应该会是:“char类型不就是char嘛”
其实不然,char类型实际上分区为有符号的signed char和无符号的unsigned char
对于char的有无符号位比较特殊的是:
在C语言中,int和char是两种不同的数据类型,它们有以下区别:
数据范围:int是整数类型,通常占用4个字节(32位),可以表示较大范围的整数,包括正数、负数和零。而char是字符类型,占用1个字节(8位),用于表示单个字符,包括字母、数字和特殊字符。
存储方式:int类型的变量以二进制补码形式存储,可以进行算术运算。char类型的变量以ASCII码形式存储,可以表示各种字符。
字面值表示:int类型的字面值可以直接写成整数形式,如10、-5等。char类型的字面值需要使用单引号括起来,如'a'、'1'等。
内存占用:int类型通常占用的内存空间比char类型更大。在一些特殊情况下,char类型可以用来节省内存空间。
运算操作:int类型可以进行各种算术运算,如加减乘除、取模等。char类型可以进行一些基本的字符操作,如比较、拼接等。
数据表示:int类型的变量可以表示整数值,如年龄、成绩等。char类型的变量可以表示字符值,如姓名的首字母、键盘输入等。
short [int]
[signed] short [int]
unsigned short [int]
int
[signed] int
unsigned int
long [int]
[signed] long [int]
unsigned long [int]
long long [int]
[signed] long long [int]
unsigned long long [int]
C 语⾔原来并没有为布尔值单独设置⼀个类型,⽽是使⽤整数 0 表⽰假,⾮零值表⽰真。在 C99 中也引⼊了 布尔类型 ,是专⻔表⽰真假的。
布尔类型的使⽤得包含头⽂件 <stdbool.h>
布尔类型变量的取值是: true或者 false.
代码演示:
_Bool flag = true;
if (flag)
printf("i like C\n");
float 单精度浮点数
double 双精度浮点数(精度更高)
long double
每⼀种数据类型都有⾃⼰的⻓度,使⽤不同的数据类型,能够创建出⻓度不同的变量,变量⻓度的不同,存储的数据范围就有所差异。
sizeof 是⼀个关键字,也是操作符,专⻔是⽤来计算sizeof的操作符数的类型⻓度的,单位是字节。
sizeof( 类型 )
sizeof 表达式
注意点:
sizeof 的计算结果是 size_t 类型的。
sizeof 运算符的返回值,C 语⾔只规定是无符号整数,并没有规定具体的类型,⽽是留给系统⾃⼰去决定, sizeof 到底返回什么类型。不同的系统中,返回值的类型有可能是unsigned int ,也有可能是 unsigned long ,甚⾄是 unsigned long long ,对应的 printf() 占位符分别是 %u 、 %lu 和 %llu 。这样不利于程序的可移植性。C 语⾔提供了⼀个解决⽅法,创造了⼀个类型别名 size_t ,⽤来统⼀表⽰ sizeof 的返回值类型。对应当前系统的 sizeof 的返回值类型,可能是 unsigned int ,也可能是unsigned long long 。
#include <stdio.h>
int main()
{
int a = 10;
printf("%zd\n", sizeof(a));
printf("%zd\n", sizeof a);//如果a是变量的名字,可以省略掉sizeof后边的()
printf("%zd\n", sizeof(int));
printf("%zd\n", sizeof(3 + 3.5));
return 0;
}
结果是
4 4 4 8
打印看看各个数据类型的所占空间
int main()
{
printf("%zd\n",sizeof(char));
printf("%zd\n",sizeof(_Bool));
printf("%zd\n",sizeof(short));
printf("%zd\n",sizeof(int));
//在c语言中规定sizeof(long)>=sizeof(int)
printf("%zd\n",sizeof(long));
printf("%zd\n",sizeof(long long));
printf("%zd\n",sizeof(float));
printf("%zd\n",sizeof(double));
printf("%zd\n",sizeof(long double));
return 0;
}
结果是:
1 1 2 4 4 8 4 8 16 (单位:字节B)
#include <stdio.h>
int main()
{
short s = 2;
int b = 10;
printf("%d\n", sizeof(s = b+1));
printf("s = %d\n", s);
return 0;
}
C 语⾔使⽤signed和unsigned关键字修饰字符型和整型类型的。
signed关键字,表⽰⼀个类型带有正负号,包含负值;unsigned关键字,表⽰该类型不带有正负号,只能表⽰零和正整数。
对于int类型,默认是带有正负号的,也就是说int等同于signed int 。
整数变量声明为 unsigned 的好处是,同样⻓度的内存能够表⽰的最⼤整数值,增⼤了⼀倍。
⽐如,16位的 signed short int 的取值范围是:-3276832767,最⼤是32767;⽽
unsigned short int 的取值范围是:065535,最⼤值增⼤到了65,535。32位的 signed
int 的取值范围可以参看limits.h中给出的定义。
signed int a;
// 等同于int a;
unsigned a;
字符类型 char 也可以设置 signed 和 unsigned 。
signed char c; // 范围为 -128 到 127
unsigned char c; // 范围为 0 到 255
注意,C 语⾔规定 char 类型默认是否带有正负号,由当前系统决定。这就是说, char 不等同于 signed char ,它有可能是 signed char ,也有可能是unsigned char 。这⼀点与 int 不同, int 就是等同于 signed int 。
上述的数据类型很多,尤其数整型类型就有short、int、long、long long 四种,为什么呢?
其实每⼀种数据类型有⾃⼰的取值范围,也就是存储的数值的最⼤值和最⼩值的区间,有了丰富的类型,我们就可以在适当的场景下去选择适合的类型。如果要查看当前系统上不同数据类型的极限值:
limits.h⽂件中说明了整型类型的取值范围。
float.h这个头⽂件中说明浮点型类型的取值范围。
为了代码的可移植性,需要知道某种整数类型的极限值时,应该尽量使⽤这些常量。
SCHAR_MIN, SCHAR_MAX :signed char 的最⼩值和最⼤值。S HRT_MIN , HRT_MAXS :short 的最⼩值和最⼤值。INT_MIN , INT_MAX :int 的最⼩值和最⼤值。LONG_MIN , LONG_MAX :long 的最⼩值和最⼤值。LLONG_MIN , LLONG_MAX :long long 的最⼩值和最⼤值。UCHAR_MAX :unsigned char 的最⼤值。USHRT_MAX :unsigned short 的最⼤值。UINT_MAX :unsigned int 的最⼤值。ULONG_MAX :unsigned long 的最⼤值。ULLONG_MAX :unsigned long long 的最⼤值。C 程序由各种令牌(token)组成,分别是
关键字是c语言内置的,关键字不是自己创建出来的,也不能自己创建
在定义变量名的时候,不能与关键字冲突
auto break case char const continue default do double else enum extern float for goto if int long register return short signed sizeof static struct switch typedef union unsigned void volatile while
auto修饰函数变量的,实际上是放在局部变量前面的,是自动变量auto int a = 10;但是所有的局部变量都是auto类型的,所以可以省略 break,continue是用来跳出循环的,通常是和for,while,do while一起出现的case是和switch,default一起出现的const表示的是常属性的意思enum-枚举,struct-结构体,union-联合体extern用来声明外部符号的goto实现跳转的语句register是寄存器的意思signed有符号的,unsigned无符号的sizeof计算大小static静态的,修饰函数变量的typedef类型重命名void无(函数的返回类型,函数的参数)volatile注意:define不是关键字,是预处理指令.
引入: 电脑上的存储设备有哪些?
寄存器(集成到CPU上) 高速缓存(cache) 内存 硬盘
从上到下
int main()
{
register int num = 3;//建议存放在寄存器中,以加快运行速度,但最终还是由编译器决定的是否存入寄存器
return 0;
}
标识符就是编程时使用的"名字",给类,接口,方法,变量,常量名,包名等起名字的字符序列
英文大小写字母、数字、下划线( _ )和美元符号( $ ) (可以使用汉字或其他合法字符命名,但是不推荐)
所有字母都大写,多个单词用下划线隔开( _ ) :MAX_VALUE
全部小写,如果有多级,用点号( . )隔开、遵循域名反写的格式:com.liyahui.demo (demo 指 包的功能)
在介绍常量的同时,介绍一下变量,注意变量不是c语言的令牌(token)
不变的值称为常量,可变的值称为变量
类型是用来创建变量的。
data_type + name
char ch = 'w'
int weight = 120
int salary = 20000
double price = 66.6
变量分为局部变量和全局变量
{}外面的变量称为全局变量,
{}内部的变量称为局部变量.
包括在另一个文件里面的,也算全局变量,但是在使用之前,要声明外部变量,使用extern+数据类型+名字来声明变量
int a = 100;
int main()
{
int a = 10;
printf("a=%d\n", a);
return 0;
}
输出结果为10,
当全局变量与局部变量冲突时,局部优先,没有局部,只能全局。不要让局部变量和全局变量取一样的名.
int main()
{
int num1 = 0;
int num2 = 0;
scanf("%d %d", &num1, &num2);
int sum = num1 + num2;
printf("%d\n", sum);
return 0;
}
&表示取地址,scanf表示扫描输入
变量的作用域(scope)是程序设计概念,通常来说,一段程序代码中所有用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域.
局部变量的作用域
局部变量的作用域是变量所在的局部范围,大包小
//这是一个错误示范
int main()
{
{
int a = 12;
printf("a=%d\n", a);
}
printf("a=%d\n", a);//这里错了
return 0;
}
局部变量只能在局部生效,第二个printf("a=%d\n", a)无法输出,会报错
int main()
{
int a = 12;
{
printf("a=%d\n", a);
}
printf("a=%d\n", a);
return 0;
}
两个a都可正常输出,因为int a = 12;对外面那个{}以内均适用
全局变量的作用域 全局可用,extern可用于外部,作用域是整个工程
int a = 12;
int main()
{
{
printf("a=%d\n", a);
}
printf("a=%d\n", a);
return 0;
}
两个a的值都可以正常输出
演示全局变量的作用域
int a = 10;
void test()
{
printf("test-->%d\n", a);
}
int main()
{
test();
{
printf("a=%d\n", a);
}
printf("a=%d\n",a);
return 0;
}
三个a的值都可正确输出
自定义函数中也可使用
声明外部变量
extern int a;
局部变量的生命周期 进入作用域到出作用域
全局变量的生命周期 整个程序的开始到结束
⼀般我们在学习C/C++语⾔的时候,我们会关注内存中的三个区域:栈区、堆区、静态区。
c语言中的常量与变量的定义形式有所差异.
(常量即为不变的量.)
c语言中的常量可以分为以下几种:
const修饰的常变量数字30,3.14浮点,整型,字符'w',字符串'abc'
在int a = 0前面加上const(adj恒定的)
在c语言中,const修饰的a,本质是变量,但是不能直接修改,有常量的属性,也有变量的属性.
const int n = 10
int arr[n] = {0}
这么做会报错,因为arr中应放常量,而const int n具有变量的成分
#define MAX 100
Max的值可以直接拿来使用,甚至可以拿来给另一个变量赋值,全局可用,可视为全局常量
也可以#define STR "abcd" ,定义为字符串
#define MAX 100
int main()
{
printf("%d\n", MAX);
int a = MAX;
printf("%d\n", a);
return 0;
}
会输出两个100
(未来的所有s可能取值)
用enum+空格+名字
enum color
{
RED
GREEN
BULE
}
int main()
{
int num = 10;
enum Color c = RED;
}
枚举常量的值不可更改
char字符类型,
'a'是字符常量,(用单引号)
char ch = 'w'
char创建的ch是字符变量,把字符常量放入字符变量当中去。"abcdef"称为字符串,用双引号引起来的一串字符称为字符串字面值,或者简称字符串.
c语言中没有字符串类型.
0-数字0
'0'-字符0 - ASCLL值是48
'\0'-字符-ASCLL值是0
EOF-end of file文件结束的标志,值是-1
字符串的结束标志是一个\0的转义字符,在计算字符串长度的时候\0是结束标志,不算作字符串内容.
使用数组来存储字符串.
数组是一组相同类型元素的集合.
例如:
char arr1[10] = "abcdef"
其中10表示可以存10个,可以省略自动分配,实际需要7个字符.
字符串的结束标志是一个\0的转译字符,所以实际上需要7个位置,会自动停止.
int main()
{
char arr1[10] = "abcdef";
char arr2[ ] = {'a','b','c','d','e','f'};
printf("%s\n", arr1);
printf("%s\n", arr2); //一定要遇到\0才会停,这种情况会乱码.
}
以上代码的结果:
abcdef
abcdefabcdef
解决办法,可以再追加'\0',以防乱码
char arr2[ ] = {'a','b','c','d','e','f','\0'};
printf("%s\n", arr2);
库函数strlen(string length),最佳解决方式,空格也算,不包括\0,
注意:需要引入头文件.
#include <string.h>
计算长度
int len = strlen("abc");
printf("%d\n", len);
或者
printf("%d\n",strlen("abc"));
注意点:strlen()里面的数组如果后面没有\0"则会输出随机数
int main()
{
char acX[] = "abcdefg";
char acY[] = { 'a','b','c','d','e','f','g'};
printf("%d\n",sizeof(acX));
printf("%d\n",sizeof(acY));
printf("%d\n",strlen(acX));
printf("%d\n",strlen(acY));
return 0;
}
结果是:
8
7
7
14(会输出随机数)
c语言是非常灵活的,C语言提供了非常丰富的操作符,使得使用起来就比较灵活.操作符就是平时用起来习以为常,但是不可或缺的符号.
+ 、- 、* 、/ 、%<< >>& | ^ = 、+= 、 -= 、 *= 、 /= 、%= 、<<= 、>>= 、&= 、|= 、^=!、++、--、&、*、+、-、~ 、sizeof、(类型) > 、>= 、< 、<= 、 == 、 !=&& 、||? :,[](). 、->有+、-、*、/、%这些
其中/是除运算,%是取模(取余数)运算.
这些操作符都是双目操作符。 他们都是有2个操作数的,位于操作符两端的就是它们的操作数,这种操作符也叫双目操作符。
演示
int main()
{
int a = 7 / 2;
//如果是float a =7/2,结果为3.0,原因就在于 C 语⾔⾥⾯的整数除法是整除,只会返回整数部分,丢弃⼩数部分。
printf("%d\n",a);
float b =7 / 2.0;
printf("%f\n",b);//除号两端都是整数的时候,执行的是整数除法,如果两端只要有一个浮点数就执行浮点数的除法.
printf("%.1f\n",b);//结果保留一位小数,`.2`则表示保留两位,以此类推.
}
再看⼀个例子:
#include <stdio.h>
int main()
{
int score = 5;
score = (score / 20) * 100;
return 0;
}
//结果为0
上⾯的代码,你可能觉得经过运算, score 会等于 25 ,但是实际上 score 等于 0 。这是因为 score / 20 是整除,会得到⼀个整数值 0 ,所以乘以 100 后得到的也是 0 。 为了得到预想的结果,可以将除数 20 改成 20.0 ,让整除变成浮点数除法。
#include <stdio.h>
int main()
{
int score = 5;
score = (score / 20.0) * 100;
return 0;
}
//结果为0
运算符 % 表⽰求模运算,即返回两个整数相除的余值。这个运算符只能⽤于整数,不能⽤于浮点数。
int c = 7 % 2;
printf("%d\n",c);//取模是不能写浮点数的
负数求模的规则是,结果的正负号由第⼀个运算数的正负号决定。
#include <stdio.h>
int main()
{
printf("%d\n", 11 % -5); // 1
printf("%d\n",-11 % -5); // -1
printf("%d\n",-11 % 5); // -1
return 0;
}
上⾯⽰例中,第⼀个运算数的正负号( 11 或 -11 )决定了结果的正负号。
<< 左移操作符>>右移操作符>> << (涉及二进制)
注:移位操作符的操作数只能是整数。
移位规则:左边抛弃、右边补0
#include <stdio.h>
int main()
{
int num = 10;
int n = num<<1;//num的值是不变的
printf("n= %d\n", n);
printf("num= %d\n", num);
return 0;
}
移位规则:首先右移运算分两种:
1.逻辑右移:左边用0填充,右边丢弃
2.算数右移:左边用原该值的符号位填充,右边丢弃
注意:对于移位运算符,不要移动负数位,这个是标准未定义的。
& ^ | ~
按位与
按位或
按位异或
按位取反
注:他们的操作数必须是整数。
#include <stdio.h>
int main()
{
int num1 = -3;
int num2 = 5;
printf("%d\n", num1 & num2);
printf("%d\n", num1 | num2);
printf("%d\n", num1 ^ num2);
printf("%d\n", ~0);
return 0;
}
⼀道变态的⾯试题: 不能创建临时变量(第三个变量),实现两个整数的交换。
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a^b;
b = a^b;
a = a^b;
printf("a = %d b = %d\n", a, b);
return 0;
}
练习1:编写代码实现:求⼀个整数存储在内存中的⼆进制中1的个数。
//⽅法1
#include <stdio.h>
int main()
{
int num = 10;
int count= 0;//计数
while(num)
{
if(num%2 == 1)
count++;
num = num/2;
}
printf("⼆进制中1的个数 = %d\n", count);
return 0;
}
//⽅法2:
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
for(i=0; i<32; i++)
{
if( num & (1 << i) )
count++;
}
printf("⼆进制中1的个数 = %d\n",count);
return 0;
}
//⽅法3:
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
while(num)
{
count++;
num = num&(num-1);
}
printf("⼆进制中1的个数 = %d\n",count);
return 0;
}
//这种⽅式是不是很好?达到了优化的效果,但是难以想到。
练习2:⼆进制位置0或者置1
编写代码将13⼆进制序列的第5位修改为1,然后再改回0
13的2进制序列: 00000000000000000000000000001101
将第5位置为1后:00000000000000000000000000011101
将第5位再置为0:00000000000000000000000000001101
参考代码:
#include <stdio.h>
int main()
{
int a = 13;
a = a | (1<<4);
printf("a = %d\n", a);
a = a & ~(1<<4);
printf("a = %d\n", a);
return 0;
}
在变量创建的时候给⼀个初始值叫初始化,在变量创建好后,再给⼀个值,这叫赋值。
int a = 100;//初始化
a = 200;//赋值,这⾥使⽤的就是赋值操作符
赋值操作符=是⼀个随时可以给变量赋值的操作符。
赋值操作符也可以连续赋值,如:
int a = 3;
int b = 5;
int c = 0;
c = b = a+3;////连续赋值,从右向左依次赋值的。
C语⾔虽然⽀持这种连续赋值,但是写出的代码不容易理解,建议还是拆开来写,这样⽅便观察代码的执⾏细节。
在写代码时,我们经常可能对⼀个数进⾏⾃增、⾃减的操作,如下代码:
int a = 10;
a += 3;
a -= 2;
复合操作符还有
= += -= *= /= &=
^= |= >>= <<=
前⾯介绍的操作符都是双⽬操作符,有2个操作数的。C语⾔中还有⼀些操作符只有⼀个操作数,被称为单⽬操作符。 ++、--、+(正)、-(负) 就是单⽬操作符的。
++是⼀种⾃增的操作符,⼜分为前置++和后置++,--是⼀种⾃减的操作符,也分为前置--和后置--.
前置++
int a = 10;
int b = ++a;//++的操作数是a,是放在a的前⾯的,就是前置++
printf("a=%d b=%d\n",a , b);
计算⼝诀:先+1,后使⽤; a原来是10,先+1,后a变成了11,再使⽤就是赋值给b,b得到的也是11,所以计算技术后,a和b都是11,相当于这样的代码:
int a = 10;
a = a+1;
b = a;
printf("a=%d b=%d\n",a , b);
后置++
int a = 10;
int b = a++;//++的操作数是a,是放在a的后⾯的,就是后置++
printf("a=%d b=%d\n",a , b);
计算⼝诀:先使⽤,后+1 a原来是10,先使⽤,就是先赋值给b,b得到了10,然后再+1,然后a变成了11,所以直接结束后a是11,b是10,相当于这样的代码:
int a = 10;
int b = a;
a = a+1;
printf("a=%d b=%d\n",a , b);
同理--前置、后置
- 负值
用来改变数字前的符号.
+ 正值
实际上没什么意义.
& 取地址(和指针有关系)
*解引用操作符
sizeof是单目操作符,求操作数的类型长度(以字节为单位)
.在结构体struct中,.为结构成员访问操作符
->在结构体struct中,->用于连接结构体指针变量和成员名称
int main()
{
int arr[10] = { 0 };//初始化数组
printf("%d\n",sizeof(arr));
//结果为40,计算的是整个数组的大小,单位是字节
printf("%d\n",sizeof(arr[10]));
//计算的结果为4,是数组中的一个元素的大小,单位是字节
printf("%d\n",sizeof(arr)/sizeof(arr[0]));
//输出的结果为10,是数组元素的个数
}
~
对一个数的二进制按位取反(后面学)
*间接访问操作符(解引用操作符)(后续会解释)
(类型)强制类型转换,括号内的类型是想要转换的类型
在c语言中,对于像3.14这样的的字面浮点数,编译器默认理解为double类型
int a = (int)3.14
printf("%d\n",a);
C 语⾔⽤于⽐较的表达式,称为 “关系表达式”(relational expression),⾥⾯使⽤的运算符就称为“关系运算符”(relational operator),主要有下⾯6个。
>大于操作符>=⼤于等于运算符 <⼩于运算符 <=⼩于等于运算符 !=不相等运算符 ==相等运算符关系表达式通常返回 0 或 1 ,表⽰真假。
C 语⾔中, 0 表⽰假,所有⾮零值表⽰真。⽐如, 20 > 12 返回 1 , 12 > 20 返回 0 。
关系表达式常⽤于 if 或 while 结构。
if (x == 3)
{
printf("x is 3.\n");
}
另⼀个需要避免的错误是:多个关系运算符不宜连⽤。
i < j < k
上⾯⽰例中,连续使⽤两个⼩于运算符。这是合法表达式,不会报错,但是通常达不到想要的结果,即不是保证变量j的值在i和 k 之间。因为关系运算符是从左到右计算,所以实际执⾏的是下⾯的表达式。
(i < j) < k
上⾯式⼦中, i < j 返回 0 或 1 ,所以最终是 0 或 1 与变量 k 进⾏⽐较。如果想要判断变量j的值是否在 i 和 k 之间,应该使⽤下⾯的写法。
i < j && j < k
&& 就是与运算符,也是并且的意思, && 是⼀个双⽬操作符,使⽤的⽅式是 a&&b , && 两边的表达式都是真的时候,整个表达式才为真,只要有⼀个是假,则整个表达式为假。
|| 就是或运算符,也就是或者的意思, || 也是⼀个双⽬操作符,使⽤的⽅式是 a || b , ||两边的表达式只要有⼀个是真,整个表达式就是真,两边的表达式都为假的时候,才为假。
int main()
{
int a = 10;
int b = 4;
if (a && b)//(a || b)
{
printf("hehe\n");
}
return 0;
}
! 逻辑反操作c语言中,0表示假
非0表示真,!的作用就是把假的变成真的,把真的变成假的
int main()
{
int flag = 0;//0表示假,非零表示真
if (!flag)//if语句中默认条件为真时执行
{
printf("haha\n");
}
}
练习 闰年的判断 输⼊⼀个年份year,判断year是否是闰年 闰年判断的规则:
#include <stdio.h>
//代码1
int main()
{
int year = 0;
scanf("%d", &year);
if(year%4==0 && year%100!=0)
printf("是闰年\n");
else if(year%400==0)
printf("是闰年\n");
return 0;
}
//代码2
int main()
{
int year = 0;
scanf("%d", &year);
if((year%4==0 && year%100!=0) ||(year%400==0))
printf("是闰年\n");
return 0;
}
C语⾔逻辑运算符还有⼀个特点,它总是先对左侧的表达式求值,再对右边的表达式求值,这个顺序是保证的。如果左边的表达式满⾜逻辑运算符的条件,就不再对右边的表达式求值。这种情况称为“短路”。 如前⾯的代码:
if(month >= 3 && month <= 5)
表达式中&& 的左操作数是 month >= 3 ,右操作数是 month <= 5 ,当左操作数 month >= 3 的结果是0的时候,即使不判断 month <= 5 ,整个表达式的结果也是0(不是春季)。 所以,对于&&操作符来说,左边操作数的结果是0的时候,右边操作数就不再执⾏。
对于 || 操作符是怎么样呢?我们结合前⾯的代码:
if(month == 12 || month==1 || month == 2)
如果month == 12,则不⽤再判断month是否等于1或者2,整个表达式的结果也是1(是冬季)。所以, || 操作符的左操作数的结果不为0时,就⽆需执⾏右操作数。像这种仅仅根据左操作数的结果就能知道整个表达式的结果,不再对右操作数进⾏计算的运算称为短路求值。
#include <stdio.h>
int main()
{
int i = 0,a=0,b=2,c =3,d=4;
i = a++ && ++b && d++;
//i = a++||++b||d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
两种情况结果分别是
a = 1
b = 2
c = 3
d = 4
和
a = 1
b = 3
c = 3
d = 4
三目操作符,有三个操作数exp1 ? exp2 : exp3
注意符号依次是:问号,冒号,分号
条件操作符的计算逻辑是:如果 exp1 为真, exp2 计算,计算的结果是整个表达式的结果;如果exp1 为假, exp3 计算,计算的结果是整个表达式的结果。
例子
int a = 10;
int b = 20;
int c = (a > b ? a : b);
逗号表达式,就是⽤逗号隔开的多个表达式。
逗号表达式,从左向右依次执⾏。整个表达式的结果是最后⼀个表达式的结果。
下面代码的结果是:
#include <stdio.h>
int main()
{
int a, b, c;
a = 5;
c = ++a;
b = ++c, c++, ++a, a++;
b += a++ + c;
printf("a = %d b = %d c = %d\n:", a, b, c);
return 0;
}
结果是 a = 9 b= 23 c = 8
//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
c是多少?
//代码2
if (a =b + 1, c=a / 2, d > 0)
//代码3
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
//...
a = get_val();
count_val(a);
}
如果使⽤逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
[]arr[3]中的[]就是下标引用操作符arr和3就是[]的操作数
操作数:⼀个数组名 + ⼀个索引值(下标)
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
//创建数组的时候,[]中不能是变量
int n = 3;
arr[n] = 20;//访问元素时,[]中可以是变量
()接受⼀个或者多个操作数:第⼀个操作数是函数名,剩余的操作数就是传递给函数的参数。
int Add(int x,int y)
{
return x+y;
}
int main()
{
int sum =Add(2,3);//()就是函数调用操作符
//Add,2,3都是()的操作数
return 0;
}
C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等;描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类型,让程序员可以⾃⼰创造适合的类型。
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚⾄是其他结构体。
struct tag
{
member-list;
}variable-list;
描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
//代码1:变量的定义
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//代码2:初始化。
struct Point p3 = {10, 20};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s1 = {"zhangsan", 20};//初始化
struct Stu s2 = {.age=20, .name="lisi"};//指定顺序初始化
//代码3
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下所⽰:
#include <stdio.h>
struct Point
{
int x;
int y;
}p = {1,2};
int main()
{
printf("x: %d y: %d\n", p.x, p.y);
return 0;
}
使⽤⽅式:结构体变量.成员名
有时候我们得到的不是⼀个结构体变量,⽽是得到了⼀个指向结构体的指针。如下所⽰:
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3, 4};
struct Point *ptr = &p;
ptr->x = 10;
ptr->y = 20;
printf("x = %d y = %d\n", ptr->x, ptr->y);
return 0;
}
使⽤⽅式:结构体指针->成员名
#include <stdio.h>
#include <string.h>
struct Stu
{
char name[15];//名字
int age; //年龄
};
void print_stu(struct Stu s)
{
printf("%s %d\n", s.name, s.age);
}
void set_stu(struct Stu* ps)
{
strcpy(ps->name, "李四");
ps->age = 28;
}
int main()
{
struct Stu s = { "张三", 20 };
print_stu(s);
set_stu(&s);
print_stu(s);
return 0;
}
C语⾔的操作符有2个重要的属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。
优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执⾏。各种运算符的优先级是不⼀样的。
3 + 4 * 5;
上⾯⽰例中,表达式 3 + 4 * 5 ⾥⾯既有加法运算符( + ),⼜有乘法运算符( * )。由于乘法的优先级⾼于加法,所以会先计算 4 * 5 ,⽽不是先计算 3 + 4 。
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执⾏顺序。⼤部分运算符是左结合(从左到右执⾏),少数运算符是右结合(从右到左执⾏),⽐如赋值运算符( = )。
5 * 6 / 2;
上⾯⽰例中, * 和 / 的优先级相同,它们都是左结合运算符,所以从左到右执⾏,先计算 5 * 6 ,再计算 6 / 2 。 运算符的优先级顺序很多,下⾯是部分运算符的优先级顺序(按照优先级从⾼到低排列),建议⼤概记住这些操作符的优先级就⾏,其他操作符在使⽤的时候查看下⾯表格就可以了。
由于圆括号的优先级最⾼,可以使⽤它改变其他运算符的优先级。
转义字符就是转变字符的意思
int main()
{
printf("abcn");
return 0;
}
输出"abcn"
int main()
{
printf("abc\n");
printf("abc\0456");
//会在\0之后刹车,\0是结束标志。
return 0;
}
输出两个abc,因为后面那个没有\n,是急停的.
\?转义防止三字母词(现在不用)??)-->]??(-->[\'用于表示字符常量' printf("%c\n",'\'');
\"用于表示一个字符内部的双引号,同样根据以上printf("\"")输出"printf("%c\n",'\"');
printf("abcd\0ef")中的\只是一个普通的\,可以printf("abcd\\0ef"),防止中途刹车printf("c:\\test\\test.c");
\n换行符printf("abc\nefg")
//输出
//abc
//efg
\t水平制表符,等于按一个tab,算一个字符,体现出来的效果是四个字符 \ddd表示1-3个八进制0~7的数字,如:\130 --->八进制的130换算成十进制后的ascall码值对应的字母printf("%c\n",'\130');
\xdd表示dd这个16进制的数字,如:\x60--->十六进制的60换算成为十进制后的ascall码值对应的字母printf("%c\n",'\x60');
转义字符长度只算一个长度,如"\t"长度算1
什么是语句? c语言中语句可分为以下5类:
#include <stdio.h>
int main()
{
int a = 20;
int b = 0;
b = a + 5; //表达式语句
return 0;
}
#include <stdio.h>
int Add(int x, int y)
{
return x+y;
}
int main()
{
printf("hehe\n");//函数调⽤语句
int ret = Add(2, 3);//函数调⽤语句
return 0;
}
#include <stdio.h>
int main()
{
;//空语句
return 0;
}
#include <stdio.h>
void print(int arr[], int sz) //函数的⼤括号中的代码也构成复合语句
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<10; i++) //for循环的循环体的⼤括号中的就是复合语句
{
arr[i] = 10-i;
printf("%d\n", arr[i]);
}
return 0;
}
以下具体讲解控制语句.
控制语句用于控制程序的执行流程,以实现程序的各种结构方式.
C语言支持三种结构:
c语言中实现选择
如果表达式中的结果为真,则语句执行. 在C语言中如何表示真假? 0表示假,非0表示真.
//无else
if(表达式)
语句;
//有else
if(表达式)
语句1;//无论是if还是else,默认后面只能跟上一条语句,后面跟上多条语句时要加上{},否则会报错.
else//表达式为假则执行else
语句2;
//多分支
if(表达式1)
语句1;
else if(表达式2)
语句2;
else (if)
语句3;
(else)
int main()
{
int input = 0;
printf("加入微软亚洲研究院\n");
printf("要好好学习吗(1/0)?");
scanf("%d", &input);
if (input == 1)
{
printf("好offer\n");
}
else
{
printf("卖红薯\n");
}
return 0;
}
已知一个函数y=f(x),x<0时,y=1,当x=0时,y=0.当x>0时,y=-1.
int main()
{
int x = 0;
int y = 0;
scanf("%d",&x);
if (x>0)
y = -1;
else if (x==0)
y = 0;
else
y = 1;
printf("%d\n",y);
return 0;
}
else的匹配:else是和它离的最近的if匹配的,并不是和谁对齐就和谁匹配.
当出现以下代码时:
#include <stdio.h>
int main()
{
int a = 0;
int b = 2;
if (a == 1)
if (b == 2)
printf("hehe\n");
else
printf("haha\n");
return 0;
}
输出结果为空
应当调整代码格式,适当的使用{}可以使代码的逻辑更加清楚,代码风格很重要.
#include <stdio.h>
int main()
{
int a = 0;
int b = 2;
if (a == 1)
{
if (b == 2)
{
printf("hehe\n");
}
}
else
{
printf("haha\n");
}
return 0;
}
if(18<= age <28)//不能这么写
if(age>=18 && age<28)//正确写法
//代码1
if (condition)
{
return x;
}
return y;
//代码2
if (condition)
{
return x;
}
else
{
return y;
}
//代码3
int num = 1;
if (num == 5)
{
printf("hehe\n");
}
//代码4
int num = 1;
if (5 == num)
{
printf("hehe\n");
}
以上代码2和4更好
switch语句也是一种分支语句.
常常用于多分支的情况.
switch(整型表达式)
{
语句项;
}
其中语句项是指一些case语句
case 整形常量表达式://注意case后面有空格
语句;
上⾯代码中,根据表达式 expression 不同的值,执⾏相应的 case 分⽀。如果找不到对应的值,就执⾏ default 分⽀。
注:
case只是决定了代码的入口,但是并没有设定代码的出口
在switch语句中的break的作用就体现了
在switch语句中,我们没办法直接实现分支.搭配break使用才能实现真正的分支.
#include <stdio.h>
int main()
{
int day = 0;
switch(day)
{
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
case 4:
printf("星期四\n");
break;
case 5:
printf("星期五\n");
break;
case 6:
printf("星期六\n");
break;
case 7:
printf("星期天\n");
break;
}
return 0;
}
多个case匹配同一个执行语句的写法:
#include <stdio.h>
//switch代码演示
int main()
{
int day = 0;
switch(day)
{
case 1:
case 2:
case 3:
case 4:
case 5:
printf("weekday\n");
break;
case 6:
case 7:
printf("weekend\n");
break;
}
return 0;
}
switch语句可以嵌套
#include <stdio.h>
int main()
{
int n = 1;
int m = 2;
switch (n)
{
case 1:
m++;
case 2:
n++;
case 3:
//switch允许嵌套使用
switch (n)
{
case 1:
n++;
case 2:
m++;
n++;
break;
}
case 4:
m++;
break;
default:
break;
}
printf("m = %d, n = %d\n", m, n);
return 0;
}
如果表达的值与所有的case标签的值都不匹配,那么可以在代码中加入default子句,把default:写在任意一个case标签可以出现的位置,当 switch 表达式的值并不匹配所有 case 标签的值时,这个 default 子句后面的语句就会执行.
所以,每个switch语句中只能出现一条default子句.
但是它可以出现在语句列表的任何位置,而且语句流会像执行一个case标签一样执行default子句.
就⽐如前⾯做的打印星期的练习,如果 day 的输⼊不是1~7的值,如果我们要提⽰:输⼊错误,则可以这样完成代码:
#include <stdio.h>
//switch代码演示
int main()
{
int day = 0;
switch(day)
{
case 1:
case 2:
case 3:
case 4:
case 5:
printf("weekday\n");
break;
case 6:
case 7:
printf("weekend\n");
break;
default:
printf("输⼊错误\n");
break;
}
return 0;
}
while(表达式)
循环语句;
int main()
{
int line = 0;
printf("加入集训队\n");
while (line < 20000)
{
printf("写代码:%d\n",line);
line++;
}
if (line >= 20000)
printf("好offer\n");
return 0;
}
练习:
输⼊⼀个正的整数,逆序打印这个整数的每⼀位 例如: 输⼊:1234,输出:4 3 2 1 输⼊:521,输出:1 2 5
题⽬解析
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
while(n)
{
printf("%d ", n%10);
n /= 10;
}
return 0;
}
#include <stdio.h>
int main()
{
int i = 1;
while(i<=10)
{
if (i == 5)
break;
printf("%d ", i);
i++;
}
return 0;
}
输出结果:1 2 3 4
总结:break在while循环中的作用:
其实在循环中只要遇到break,就停止后期的所有的循环,直接终止循环.
所以:while中的break是用于永久终止循环的
//continue 代码实例1
#include <stdio.h>
int main()
{
int i = 1;
while(i<=10)
{
if (i == 5)
continue;
printf("%d ",i);
i = i+1;
}
return 0;
}
以上代码输出结果为1 2 3 4
//continue 代码实例2
#include <stdio.h>
int main()
{
int i = 1;
while(i<=10)
{
i = i+1;
if (i == 5)
continue;
printf("%d ", i);
}
return 0;
}
输出结果为:2 3 4 6 7 8 9 10 11
总结:
continue在while循环中的作用
continue是用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行,而是直接跳转到while语句的判断部分.进行下一次循环的入口判断.
for(表达式1; 表达式2; 表达式3)
循环语句;
例子:使用for循环 在屏幕上打印1-10的数字
#include <stdio.h>
int main()
{
int i = 0;
//for(i=1/*初始化*/; i<=10/*判断部分*/; i++/*调整部分*/)
for(i=1; i<=10; i++)
{
printf("%d ", i);
}
return 0;
}
int i = 0;
//实现相同的功能,使用while
i=1;//初始化部分
while(i<=10)//判断部分
{
printf("hehe\n");
i = i+1;//调整部分
}
//实现相同的功能,使用for
for(i=1; i<=10; i++)
{
printf("hehe\n");
}
可以发现在while循环中依然存在循环的三个必须条件,但是由于风格的问题使得三个部分很可能偏离较远,这样查找修改就不够集中和方便。所以,for循环的风格更胜一筹;for循环使用的频率也最高。
for如果有多层嵌套,break只能结束当前所在的for循环,而不是所有循环.
在for循环中也可以出现break和continue,他们的意义和在while循环中是一样的。但是还是有些差异:
//代码1
#include <stdio.h>
int main()
{
int i = 0;
for(i=1; i<=10; i++)
{
if(i == 5)
break;
printf("%d ",i);
}
return 0;
}
//代码2
#include <stdio.h>
int main()
{
int i = 0;
for(i=1; i<=10; i++)
{
if(i == 5)
continue;
printf("%d ",i);
}
return 0;
}
建议:
int i = 0;
//前闭后开的写法
for(i=0; i<10; i++)
{
;
}
//两边都是闭区间
for(i=0; i<=9; i++)
{
;
}
代码1
#include <stdio.h>
int main()
{
for(;;)
{
printf("hehe\n");
}
//for循环中的初始化部分,判断部分,调整部分是可以省略的,但是不建议初学时省略,容易导致问题。
//省略判断语句会导致程序死循环(判断条件始终为真)
}
代码2
#include <stdio.h>
int main()
{
int i = 0;
int j = 0;
for(i=0; i<10; i++)
{
for(j=0; j<10; j++)
{
printf("hehe\n");
}
}
//这里打印多少个hehe?100个
}
代码3
#include <stdio.h>
int main()
{
int i = 0;
int j = 0;
for(; i<10; i++)
{
for(; j<10; j++)
{
printf("hehe\n");
}
}
//如果省略掉初始化部分,这里打印多少个hehe?10个
}
代码4
(使用多余一个变量控制循环)
#include <stdio.h>
int main()
{
int x, y;
for (x = 0, y = 0; x<2 && y<5; ++x, y++)
{
printf("hehe\n");
}
return 0;
}
一道笔试题
//请问循环要循环多少次?
#include <stdio.h>
int main()
{
int i = 0;
int k = 0;
for(i =0,k=0; k=0; i++,k++)
k++;
return 0;
}//循环0次,因为判断部分其实是赋值,赋值是用一个"=",而0又代表是假,所以for循环条件不成立,不循环.
//如果是两个"=",那么只循环一次.
do语句的语法
do
循环语句;
while(表达式);
循环至少执行一次,使用的场景有限,所以不是经常使用
#include <stdio.h>
int main()
{
int i = 10;
do
{
printf("%d\n", i);
i++;
}
while(i<10);
return 0;
}
输⼊⼀个正整数,计算这个整数是⼏位数? 例如: 输⼊:1234 输出:4 输⼊:12 输出:2
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int cnt = 0;
do
{
cnt++;
n = n / 10;
} while (n);
printf("%d\n", cnt);
return 0;
}
这⾥并⾮必须使⽤ do while 语句,但是这个代码就⽐较适合使⽤ do while 循环,因为n即使是0,也是1位数,要统计位数的。
break的使用
#include <stdio.h>
int main()
{
int i = 1;
do
{
if(5 == i)
break;
printf("%d\n", i);
i++;
}
while(i<10);
return 0;
}//输出结果1234
continue的使用
#include <stdio.h>
int main()
{
int i = 1;
do
{
if(5 == i)
continue;
printf("%d\n", i);
i++;
}
while(i<10);
return 0;
}//输出结果1234
int factorial(int para)
{
int i = 0;
int a = 1;
for(i=1;i<=para;i++)
a=a*i;
return a;
}
int main()
{
int n = 0;
scanf("%d",&n);
printf("%d\n",factorial(n));
return 0;
}
结果:4037913
int main()
{
int i = 0;
int a = 1;
int sum = 0;
for(i=1;i<=10;i++)
{
a *= i;
sum += a;
}
printf("%d",sum);
return 0;
}
int main()
{
int n = 0;
int i = 0;
int a = 1;
int output = 0;
for(n=1;n<=10;n++)
{
for(i=1;i<=n;i++)
{
a=a*i;
}
output = output + a;
a = 1;
}
printf("%d",output);
return 0;
}
二分查找算法演示
int bin_search(int arr[], int left, int right, int key)
{
int mid = 0;
while(left<=right)
{
mid = (left+right)>>1;
if(arr[mid]>key)
{
right = mid-1;
}
else if(arr[mid] < key)
{
left = mid+1;
}
else
return mid;//找到了,返回下标
}
retrun -1;//找不到
}
int main()
{
int arr[] ={1,2,3,4,5,6,7,8,9,10};
int key = 7;
int sz = sizeof(arr)/sizeof(arr[0]);
int left = 0;
int right = sz - 1;
while(left<=right)//注意=不能少
{
int mid = (left+right)/2;//这个一定要放在循环里面,否则构不成二分查找,同时这种写法容易导致溢出,所以要换一种写法
//int mid =left+(right-left)/2;
if(arr[mid]<key)
{
left=mid+1;
}
else if(arr[mid]>key)
{
right=mid-1;
}
else
{
printf("找到了,下标是%d\n",mid);
break;
}
}
if(left>right)
{
printf("找不到\n");
}
return 0;
}
int main()
{
char arr1[] = {"Talk is cheap,show me the code"};
char arr2[] = {"##############################"};
int len = strlen(arr1);
int left = 0;
int right = len - 1;
while(left<right)
{
arr2[left]=arr1[left];
arr2[right]=arr1[right];
sleep(1000);
left++;
right--;
printf("%s\n",arr2);
}
return 0;
}
int main()
{
char arr[] ={0};
char key[] ="abcd123456";
int chance = 3;
while(chance>0)
{
printf("请输入密码\n");
scanf("%s",arr);//这里无需取地址
if(strcmp(arr,key)==0)//比较两个字符串的大小,应当使用strcmp()函数
{
printf("登陆成功");
break;
}
else
{
printf("密码错误,还有%d次机会",chance);
chance--;
}
}
if(chance==0)
{
printf("机会已用完\n");
}
return 0;
}
游戏要求:
随机数⽣成:
要想完成猜数字游戏,⾸先得产⽣随机数,那怎么产⽣随机数呢?
C语⾔提供了⼀个函数叫 rand,这函数是可以⽣成随机数的
int rand (void);
rand函数会返回⼀个伪随机数,这个随机数的范围是在0~RAND_MAX之间,这个RAND_MAX的⼤⼩是依赖编译器上实现的,但是⼤部分编译器上是32767。
rand函数的使⽤需要包含⼀个头⽂件是:stdlib.h
那我们就测试⼀下rand函数,这⾥多调⽤⼏次,产⽣5个随机数:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
return 0;
}
其实rand函数⽣成的随机数是伪随机的,伪随机数不是真正 的随机数,是通过某种算法⽣成的随机数。真正的随机数的是⽆法预测下⼀个值是多少的。⽽rand函数是对⼀个叫“种子”的基准值进⾏运算⽣成的随机数。之所以前⾯每次运⾏程序产⽣的随机数序列是⼀样的,那是因为rand函数⽣成随机数的默认种⼦是1。如果要⽣成不同的随机数,就要让种⼦是变化的。
C语⾔中⼜提供了⼀个函数叫srand,⽤来初始化随机数的⽣成器的,srand的原型如下:
void srand (unsigned int seed);
程序中在调⽤ rand 函数之前先调⽤ srand 函数,通过 srand 函数的参数seed来设置rand函数⽣成随机数的时候的种⼦,只要种⼦在变化,每次⽣成的随机数序列也就变化起来了。那也就是说给srand的种⼦是如果是随机的,rand就能⽣成随机数;在⽣成随机数的时候⼜需要⼀个随机数,这就⽭盾了。
在程序中我们⼀般是使⽤程序运⾏的时间作为种⼦的,因为时间时刻在发⽣变化的。在C语⾔中有⼀个函数叫time,就可以获得这个时间,time函数原型如下:
time_t time (time_t* timer);
time 函数会返回当前的⽇历时间,其实返回的是1970年1⽉1⽇0时0分0秒到现在程序运⾏时间之间的差值,单位是秒。返回的类型是time_t类型的,time_t 类型本质上其实就是32位或者64位的整型类型。
time函数的参数 timer 如果是⾮NULL的指针的话,函数也会将这个返回的差值放在timer指向的内存中带回去。
如果 timer 是NULL,就只返回这个时间的差值。time函数返回的这个时间差也被叫做:时间戳。
time函数的时候需要包含头⽂件:time.h
如果只是让time函数返回时间戳,我们就可以这样写:
time(NULL);//调⽤time函数返回时间戳,这⾥没有接收返回值
那我们就可以让⽣成随机数的代码改写成如下:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
//使⽤time函数的返回值设置种⼦
//因为srand的参数是unsigned int类型,我们将time函数的返回值强制类型转换
srand((unsigned int)time(NULL));
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
}
应当注重这句代码:
srand((unsigned int)time(NULL));
srand函数是不需要频繁调⽤的,⼀次运⾏的程序中调⽤⼀次就够了。
设置随机数的范围:
rand() % 100;//余数的范围是0~99
rand()%100+1;//%100的余数是0~99,0~99的数字+1,范围是1~100
100 + rand()%(200-100+1);//余数的范围是0~100,加100后就是100~200
a + rand()%(b-a+1);
猜数字游戏实现:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void menu()
{
printf("**********************************\n");
printf("*********** 1.play **********\n");
printf("*********** 0.exit **********\n");
printf("**********************************\n");
}
//RAND_MAX(0-32767)--rand函数能返回随机数的最大值。
void game()
{
int random_num = rand()%100+1;
//使用rand()函数生成随机数,但是前提是要用srand函数
//这里的rand()%100生成的值为0-99之间的数字,加上1之后的数字才是1-100
int input = 0;
int count = 5;
while(count)
{
printf("请输入猜的数字>:");
scanf("%d", &input);
if(input > random_num)
{
printf("猜大了\n");
}
else if(input < random_num)
{
printf("猜小了\n");
}
else
{
printf("恭喜你,猜对了\n");
break;
}
count--;
printf("\n你还有%d次机会\n", count);
}
if (count == 0)
{
printf("你失败了,正确值是:%d\n", r);
}
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));//时间戳当作随机数,NULL是空指针
do
{
menu();
printf("请选择>:");
scanf("%d", &input);
switch(input)
{
case 1:
game();
break;
case 0:
break;
default:
printf("选择错误,请重新输入!\n");
break;
}
}while(input);
return 0;
}
C语言中提供了可以随意滥用的 goto语句和标记跳转的标号.
从理论上 goto语句是没有必要的,实践中没有goto语句也可以很容易的写出代码.
但是某些场合下goto语句还是用得着的,最常见的用法就是终止程序在某些深度嵌套的结构的处理过程.
例如:一次跳出两层或多层循环.
多层循环这种情况使用break是达不到目的的。它只能从最内层循环退出到上一层的循环.
for(...)
for(...)
{
for(...)
{
if(disaster)
goto error;
}
}
error:
if(disaster)
// 处理错误情况
下面是使用goto语句的一个例子,然后使用循环的实现方式替换goto语句: 一个关机程序
#include <stdio.h>
int main()
{
char input[10] = {0};
system("shutdown -s -t 60");
again:
printf("电脑将在1分钟内关机,如果输入:我是猪,就取消关机!\n请输入:>");
scanf("%s", input);
if(0 == strcmp(input, "我是猪"))
{
system("shutdown -a");
}
else
{
goto again;
}
return 0;
}
而如果不适用goto语句,则可以使用循环;
#include <stdio.h>
#include <stdlib.h>
int main()
{
char input[10] = {0};
system("shutdown -s -t 60");
while(1)
{
printf("电脑将在1分钟内关机,如果输入:我是猪,就取消关机!\n请输入:>");
scanf("%s", input);
if(0 == strcmp(input, "我是猪"))
{
system("shutdown -a");
break;
}
}
return 0;
}
c语言的注释有两种方法
第⼀种⽅法是将注释放在 /.../ 之间,内部可以分⾏。 不能在注释内嵌套注释,注释也不能出现在字符串或字符值中。
/* 单行注释 */
/*
多行注释
多行注释
多行注释
*/
以下这种注释是正确的·
int fopen(char* s /* file name */, int mode);
以 // 开始的单行注释,这种注释可以单独占一行。
int x = 1; // 这也是注释
不管是哪⼀种注释,都不能放在双引号⾥⾯。
双引号⾥⾯的注释符号,会成为字符串的⼀部分,解释为普通符号,失去注释作⽤。
printf("// hello /* world */ ");
上面示例中,双引号⾥⾯的注释符号,都会被视为普通字符,没有注释作⽤。
编译时,注释会被替换成⼀个空格,所以min/* 这⾥是注释*/Value会变成min Value,⽽不是minValue。
目录
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员开发软件 简单的总结,C语言常用的库函数都有:
自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
函数的组成:
ret_type fun_name(para1, * )
//当省略不写ret_type时,默认是返回值是int类型的
{
statement;
}
ret_type 返回类型(return)
fun_name 函数名(function)
para1 函数参数(parameter)
statement 语句
一段求两个数的和的程序
int main()
{
int n1 = 0;
int n2 = 0;
scanf("%d %d", &n1, &n2);
int sum = n1 + n2;
printf("%d\n", sum);
return 0;
}
使用自己定义的函数
int Add(int x,int y) //int是指返回类型
{
int z =0;
z = x + y;
return z;
}
int main()
{
int n1 = 0;
int n2 = 0;
scanf("%d %d", &n1, &n2);
int sum = Add(n1,n2);
printf("%d\n", sum);
return 0;
}
注意:当不需要输出返回值的时候,这个时候往往会将自定义函数名称前面加上void,表示无返回值.
写一个函数可以找出两个整数中的最大值
#include <stdio.h>
//get_max函数的设计
int get_max(int x, int y)
{
return (x>y)?(x):(y);
}
int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);
printf("max = %d\n", max);
return 0;
}
在函数的设计中,函数中经常会出现return语句,这⾥讲⼀下return语句使⽤的注意事项。
int c = Add(10,b);
int c = Add(a+3,b);
int c = Add(Add(a,3),b);
//以上均正确
//无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给**形参**。
写一个函数可以交换两个整形变量的内容。
#include <stdio.h>
//实现成函数,但是不能完成任务
void Swap1(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
//当实参传递给形参的时候,形参是实参的一份临时拷贝。
//对形参的改变不能改变实参
//正确的版本
void Swap2(int *px, int *py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1: num1 = %d num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("Swap2: num1 = %d num2 = %d\n", num1, num2);
return 0;
}
上面Swap1和Swap2函数中的参数x,y,px,py都是形式参数。在main函数中传给 Swap1 的 num1 ,num2 和传给 Swap2 函数的 &num1 , &num2 是实际参数。
这里我们对函数的实参和形参进行分析:Swap1 函数在调用的时候, x , y 拥有自己的空间,同时拥有了和实参一模一样的内容。
所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。
在使⽤函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进⾏操作。
⽐如:写⼀个函数对将⼀个整型数组的内容,全部置为-1,再写⼀个函数打印数组的内容。简单思考⼀下,基本的形式应该是这样的:
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
set_arr();//设置数组内容为-1
print_arr();//打印数组内容
return 0;
}
这⾥的set_arr函数要能够对数组内容进⾏设置,就得把数组作为参数传递给函数,同时函数内部在设置数组每个元素的时候,也得遍历数组,需要知道数组的元素个数。所以我们需要给set_arr传递2个参数,⼀个是数组,另外⼀个是数组的元素个数。仔细分析print_arr也是⼀样的,只有拿到了数组和元素个数,才能遍历打印数组的每个元素。
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int sz = sizeof(arr)/sizeof(arr[0]);
set_arr(arr, sz);//设置数组内容为-1
print_arr(arr, sz);//打印数组内容
return 0;
}
数组作为参数传递给了set_arr 和 print_arr 函数了,那这两个函数应该如何设计呢? 这⾥我们需要知道数组传参的⼏个重点知识:
根据上述的信息,我们就可以实现这两个函数:
void set_arr(int arr[], int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
arr[i] = -1;
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
函数的调用分为传值调用和传址调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
int is_prime(int x)
{
int flag = 0;
int i = 0;
for(i=2;i<=sqrt(x);i++)
//学习点
{
if(x % i == 0)
{
flag=1;
break;
}
}
return flag;
}
int main()
{
int input = 0;
printf("%s\n","请输入数字");
scanf("%d",&input);
if(is_prime(input)==0)
{
printf("%s\n","是素数");
}
else
{
printf("%s\n","不是素数");
}
return 0;
}
int is_run(int year)
{
flag =0;
if((year%4==0)&&(year%100!=0)||(year%400==0))
//学习点
{
flag = 1;
}
return flag;
}
int main()
{
int input = 0;
printf("%s\n","请输入年份");
scanf("%d",&input);
if(is_run(input)==0)
{
printf("%s\n","不是闰年");
}
else
{
printf("%s\n","是闰年");
}
return 0;
}
int binary_search(int arr[],int key,int sz)
{
//形参arr实际上是指针变量
//知识点:数组传参实际上传的是数组首元素的地址
//所以在数组内部计算一个函数参数部分的数组元素个数是不行的
int left,i= 0;
int right = sz-1;
while(left<=right)
{
for(i=0;i<sz;i++)
{
int mid=left+(right-left)/2;
if(arr[mid]<key)
{
left=mid+1;
}
else if(arr[mid]>key)
{
right=mid-1;
}
else
{
return 1;
}
}
}
return -1;
}
int main()
{
int arr1[]={0,1,2,3,4,5,6,7,8,9};
int sz = sizeof(arr1)/sizeof(arr1[0]);
int key = 0;
printf("%s\n","输入key的值");
scanf("%d",&key);
int ret =binary_search(arr1,key,sz);
if(ret==1)
{
printf("%s","找到了");
}
else
{
printf("%s","找不到");
}
return 0;
}
void Add(int* p)
{
(*p)++;
}
int main()
{
int num = 0;
Add(&num);
printf("%d\n",num);
Add(&num);
printf("%d\n",num);
return 0;
}
以上代码等价于
int Add(int n)
{
return ++n;
}
int main()
{
int num = 0;
num = Add(num);
printf("%d\n",num);
num = Add(num);
printf("%d\n",num);
return 0;
}
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for(i=0; i<3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
函数可以嵌套调用,但是不能嵌套定义。
函数与函数之间的地位是平等的,一个函数不能在另一个函数内被调用。
把一个函数的返回值作为另外一个函数的参数。
链式访问的前提是函数要有返回值。
int main()
{
int len =strlen("abcdef");
printf("%d\n",len);
printf("%d\n",strlen("abcdef"));
//这就是链式访问
return 0;
}
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"bit"));
//这里介绍一下strlen函数
printf("%d\n", ret);
return 0;
}
判断以下程序的结果是什么?
int main()
{
printf("%d",printf("%d",printf("%d",43)));
return 0;
}
结果是4321
⼀般我们在使⽤函数的时候,直接将函数写出来就使⽤了。 ⽐如:我们要写⼀个函数判断⼀年是否是闰年。
#include <stido.h>
//判断⼀年是不是闰年
int is_leap_year(int y)
{
if(((y%4==0)&&(y%100!=0)) || (y%400==0))
return 1;
else
return 0;
}
int main()
{
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y);
if(r == 1)
printf("闰年\n");
else
printf("⾮闰年\n");
return 0;
}
上⾯代码中橙⾊的部分是函数的定义,绿⾊的部分是函数的调⽤。 这种场景下是函数的定义在函数调⽤之前,没啥问题。
那如果我们将函数的定义放在函数的调⽤后边,如下:
#include <stido.h>
int main()
{
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y);
if(r == 1)
printf("闰年\n");
else
printf("⾮闰年\n");
return 0;
}
//判断⼀年是不是闰年
int is_leap_year(int y)
{
if(((y%4==0)&&(y%100!=0)) || (y%400==0))
return 1;
else
return 0;
}
这个代码在VS2022上编译,会出现下⾯的警告信息:
这是因为C语⾔编译器对源代码进⾏编译的时候,从第⼀⾏往下扫描的,当遇到第7⾏的is_leap_year函数调⽤的时候,并没有发现前⾯有is_leap_year的定义,就报出了上述的警告。
怎么解决这个问题呢?就是函数调⽤之前先声明⼀下is_leap_year这个函数,声明函数只要交代清楚:函数名,函数的返回类型和函数的参数。如:
int is_leap_year(int y);
这就是函数声明,函数声明中参数只保留类型,省略掉名字也是可以的。
代码变成这样就能正常编译了。
#include <stdio.h>
int is_leap_year(int y);//函数声明
int main()
{
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y);
if(r == 1)
printf("闰年\n");
else
printf("⾮闰年\n");
return 0;
}
//判断⼀年是不是闰年
int is_leap_year(int y)
{
if(((y%4==0)&&(y%100!=0)) || (y%400==0))
return 1;
else
return 0;
}
函数的调⽤⼀定要满⾜,先声明后使⽤;
函数的定义也是⼀种特殊的声明,所以如果函数定义放在调⽤之前也是可以的。
⼀般在企业中我们写代码时候,代码可能⽐较多,不会将所有的代码都放在⼀个⽂件中;我们往往会根据程序的功能,将代码拆分放在多个⽂件中。
⼀般情况下,函数的声明、类型的声明放在头⽂件(.h)中,函数的实现是放在源⽂件(.c)⽂件中。
如下:
add.c
//函数的定义
int Add(int x, int y)
{
return x+y;
}
add.h
//函数的声明
int Add(int x, int y);
test.c
#include <stdio.h>
#include "add.h"
int main()
{
int a = 10;
int b = 20;
//函数调⽤
int c = Add(a, b);
printf("%d\n", c);
return 0;
}
static 和 extern 都是C语⾔中的关键字。
static 是 静态的 的意思,可以⽤来:
在讲解 static 和 extern 之前再讲⼀下:作⽤域和⽣命周期。
**作⽤域(scope)**是程序设计概念,通常来说,⼀段程序代码中所⽤到的名字并不总是有效(可⽤)的,⽽限定这个名字的可⽤性的代码范围就是这个名字的作⽤域。
⽣命周期指的是变量的创建(申请内存)到变量的销毁(收回内存)之间的⼀个时间段。
//代码1
#include <stdio.h>
void test()
{
int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for(i=0; i<5; i++)
{
test();
}
return 0;
}
//代码2
#include <stdio.h>
void test()
{
//static修饰局部变量
static int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for(i=0; i<5; i++)
{
test();
}
return 0;
}
对⽐代码1和代码2的效果,理解 static 修饰局部变量的意义。
代码1的test函数中的局部变量i是每次进⼊test函数先创建变量(⽣命周期开始)并赋值为0,然后++,再打印,出函数的时候变量⽣命周期将要结束(释放内存)。
代码2中,我们从输出结果来看,i的值有累加的效果,其实 test函数中的i创建好后,出函数的时候是不会销毁的,重新进⼊函数也就不会重新创建变量,直接上次累积的数值继续计算。
结论:static修饰局部变量改变了变量的⽣命周期,⽣命周期改变的本质是改变了变量的存储类型,本来⼀个局部变量是存储在内存的栈区的,但是被 static 修饰后存储到了静态区。存储在静态区的变量和全局变量是⼀样的,⽣命周期就和程序的⽣命周期⼀样了,只有程序结束,变量才销毁,内存才回收。但是作⽤域不变的。
使⽤建议:未来⼀个变量出了函数后,我们还想保留值,等下次进⼊函数继续使⽤,就可以使⽤static修饰。
代码一:
add.c
int g_val = 2018;
test.c
#include <stdio.h>
extern int g_val;
int main()
{
printf("%d\n", g_val);
return 0;
}
代码二: add.c
static int g_val = 2018;
test.c
#include <stdio.h>
extern int g_val;
int main()
{
printf("%d\n", g_val);
return 0;
}
extern 是⽤来声明外部符号的,如果⼀个全局的符号在A⽂件中定义的,在B⽂件中想使⽤,就可以使⽤ extern 进⾏声明,然后使⽤。
代码1正常,代码2在编译的时候会出现链接性错误。
结论:
⼀个全局变量被static修饰,使得这个全局变量只能在本源⽂件内使⽤,不能在其他源⽂件内使⽤。
本质原因是全局变量默认是具有外部链接属性的,在外部的⽂件中想使⽤,只要适当的声明就可以使⽤;但是全局变量被 static 修饰之后,外部链接属性就变成了内部链接属性,只能在⾃⼰所在的源
⽂件内部使⽤了,其他源⽂件,即使声明了,也是⽆法正常使⽤的。
使⽤建议:如果⼀个全局变量,只想在所在的源⽂件内部使⽤,不想被其他⽂件发现,就可以使⽤static修饰。
编写大型工程时,往往会把自定义函数放在另外一个源文件中,这样会更加清晰. 代码1(无static修饰)
//add.c
int Add(int x, int y)
{
return x+y;
}
//test.c
extern int Add(int x, int y);
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
代码2(有static修饰)
//add.c
static int Add(int x, int y)
{
return x+y;
}
//test.c
extern int Add(int x, int y);
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
代码1正常,代码2在编译的时候会出现连接性错误. 结论: 其实 static 修饰函数和 static 修饰全局变量是⼀模⼀样的,⼀个函数在整个⼯程都可以使⽤,被static修饰后,只能在本⽂件内部使⽤,其他⽂件⽆法正常的链接使⽤了。 本质是因为函数默认是具有外部链接属性,具有外部链接属性,使得函数在整个⼯程中只要适当的声明就可以被使⽤。但是被 static 修饰后变成了内部链接属性,使得函数只能在⾃⼰所在源⽂件内部 使⽤。
使⽤建议:⼀个函数只想在所在的源⽂件内部使⽤,不想被其他源⽂件使⽤,就可以使⽤ static 修饰。
函数⾃⼰调⽤⾃⼰。
#include <stdio.h>
int main()
{
printf("hehe\n");
main();//main函数中⼜调⽤了main函数
return 0;
}
上述就是⼀个简单的递归程序,只不过上⾯的递归只是为了演⽰递归的基本形式,不是为了解决问题,代码最终也会陷⼊死递归,导致栈溢出(Stack overflow)。
把⼀个⼤型复杂问题层层转化为⼀个与原问题相似,但规模较⼩的⼦问题来求解;直到⼦问题不能再被拆分,递归就结束了。所以递归的思考⽅式就是把⼤事化⼩的过程。
递归中的递就是递推的意思,归就是回归的意思,接下来慢慢来体会。
递归在书写的时候,有2个必要条件:
int Fact(int n)
{
if(n==0)
return 1;
else
return n*Fact(n-1);
}
void Print(int n)
{
if(n>9)
{
Print(n/10);
}
printf("%d ", n%10);
}
int main()
{
int m = 0;
scanf("%d", &m);
Print(m);
return 0;
}
int Fib(int n)
{
if(n<=2)
return 1;
else
return Fib(n-1)+Fib(n-2);
}
其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计算,⽽且递归层次越深,冗余计算就会越多。
递归是⼀种很好的编程技巧,但是和很多技巧⼀样,也是可能被误⽤的,就像举例1⼀样,看到推导的公式,很容易就被写成递归的形式:
int Fact(int n)
{
if(n==0)
return 1;
else
return n*Fact(n-1);
}
Fact函数是可以产⽣正确的结果,但是在递归函数调⽤的过程中涉及⼀些运⾏时的开销。
在C语⾔中每⼀次函数调⽤,都要需要为本次函数调⽤在栈区申请⼀块内存空间来保存函数调⽤期间的各种局部变量的值,这块空间被称为运⾏时堆栈,或者函数栈帧。 函数不返回,函数对应的栈帧空间就⼀直占⽤,所以如果函数调⽤中存在递归调⽤的话,每⼀次递归 函数调⽤都会开辟属于⾃⼰的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。 所以如果采⽤函数递归的⽅式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题。
所以如果不想使⽤递归就得想其他的办法,通常就是迭代的⽅式(通常就是循环的⽅式)。 ⽐如:计算n的阶乘,也是可以产⽣1~n的数字累计乘在⼀起的。
int Fact(int n)
{
int i = 0;
int ret = 1;
for(i=1; i<=n; i++)
{
ret *= i;
}
return ret;
}
上述代码是能够完成任务,并且效率是⽐递归的⽅式更好的。
事实上,我们看到的许多问题是以递归的形式进⾏解释的,这只是因为它⽐⾮递归的形式更加清晰,但是这些问题的迭代实现往往⽐递归实现效率更⾼。
当⼀个问题⾮常复杂,难以使⽤迭代的⽅式实现时,此时递归实现的简洁性便可以补偿它所带来的运⾏时开销。
再比如斐波那契数列
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while(n>2)
{
c = a+b;
a = b;
b = c;
n--;
}
return c;
}
要存储1-10的数字,怎么存储?
C 语言支持数组数据结构,它可以存储一个固定大小(存在溢出问题)的相同类型元素的顺序集合.数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量.
注意点:
变量名+[i]来使用一个数组在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:
type_t arr_name [const_n];
//type_t 是指数组的元素类型
//const_n 是一个常量表达式,用来指定数组的大小
这叫做一维数组。const_n必须是一个大于零的整数常量,type_t 可以是任意有效的 C 数据类型。
//代码1
int arr1[10];
//代码2
int count = 10;
int arr2[count];//数组时候可以正常创建?不能
//代码3
char arr3[10];
float arr4[1];
double arr5[20];
注:数组创建,在C99标准之前, [] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数组的概念。
int n = 10;
scanf("%d",&n);//手动输入数字,确定数组大小
int arr[n];
上⾯⽰例中,数组 arr 就是变⻓数组,因为它的⻓度取决于变量 n 的值,编译器没法事先确定,只有运⾏时才能知道 n 是多少。 变⻓数组的根本特征,就是数组⻓度只有运⾏时才能确定,所以变⻓数组不能初始化。它的好处是程序员不必在开发时,随意为数组指定⼀个估计的⻓度,程序可以在运⾏时为数组分配精确的⻓度。有 ⼀个⽐较迷惑的点,变⻓数组的意思是数组的⼤⼩是可以使⽤变量来指定的,在程序运⾏的时候,根据变量的⼤⼩来指定数组的元素个数,⽽不是说数组的⼤⼩是可变的。数组的⼤⼩⼀旦确定就不能再 变化了。
数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值。
在 C 中,您可以逐个初始化数组,也可以使用一个初始化语句,如下所示:
int arr[10] = {0};
int arr1[10] = {1,2,3};
//不完全初始化,剩余的元素默认初始化为0
int arr2[] = {1,2,3,4};
int arr3[5] = {1,2,3,4,5};
char arr4[3] = {'a',98, 'c'};
char arr5[] = {'a','b','c'};
char arr6[] = "abcdef";//只有字符串可以这样写
下面的代码要区分,内存中如何分配
char arr1[10] = "abc";
//a b c \0 0 0 0 0 0 0
char arr2[10] = {'a','b','c'};
//a b c 0 0 0 0 0 0 0
char arr3[]="abc";
//a b c \0
char arr4[]={'a','b','c'};
//a b c
数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。
double salary = balance[9];
实例1:
int main()
{
int i = 0;
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
for(i=0; i<10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
实例2:
#include <stdio.h>
int main ()
{
int n[ 10 ]; //是一个包含 10 个整数的数组 //
int i,j;
/* 初始化数组元素 */
for ( i = 0; i < 10; i++ )
{
n[ i ] = i + 100; /* 设置元素 i 为 i + 100 */
}
/* 输出数组中每个元素的值 */
for (j = 0; j < 10; j++ )
{
printf("Element[%d] = %d\n", j, n[j] );
}
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Element[0] = 100
Element[1] = 101
Element[2] = 102
Element[3] = 103
Element[4] = 104
Element[5] = 105
Element[6] = 106
Element[7] = 107
Element[8] = 108
Element[9] = 109
实例3
#include <stdio.h>
int main()
{
int arr[10] = {0};
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; ++i)
{
printf("&arr[%d] = %p\n", i, &arr[i]);
}
return 0;
}
分两种情况:
使用 sizeof 运算符来获取数组的长度
int arr[] ={0,1,2,3,4,5}
int sz = sizeof(arr)/sizeof(arr[0]);
使用宏定义
#include <stdio.h>
#define LENGTH(array) (sizeof(array) / sizeof(array[0]))
int main()
{
int array[] = {1, 2, 3, 4, 5};
int length = LENGTH(array);
printf("数组长度为: %d\n", length);
return 0;
}
strlen(arr1);
#include <stdio.h>
int main()
{
int arr[10] = {0};
int i = 0;
for(i=0; i<10; i++)
{
scanf("%d", &arr[i]);
}
return 0;
}
C 语言支持多维数组。多维数组声明的一般形式如下:
type name[size1][size2]...[sizeN];
例如,下面的声明创建了一个三维 5 . 10 . 4 整型数组:
int threedim[5][10][4];
多维数组最简单的形式是二维数组。一个二维数组,在本质上,是一个一维数组的列表。
形式如下:
type arrayName [ x ][ y ];
其中,type 可以是任意er有效的 C 数据类型,arrayName 是一个有效的 C 标识符。一个二维数组可以被认为是一个带有 x 行和 y 列的表格。下面是一个二维数组,包含 3 行和 4 列:
int x[3][4];
int arr[3][4];
char arr[3][5];
double arr[2][4];
int arr1[3][5] = {1,2};
int arr2[3][5] = {0};
int arr3[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};
int arr4[3][5] = {{1,2},{3,4},{5,6}};
初始化时省略⾏,但是不能省略列
int arr5[][5] = {1,2,3};
int arr6[][5] = {1,2,3,4,5,6,7};
int arr7[][5] = {{1,2}, {3,4}, {5,6}};
二维数组的使用也是通过下标的方式。
#include <stdio.h>
int main()
{
int arr[3][4] = {0};
int i = 0;
for(i=0; i<3; i++)
{
int j = 0;
for(j=0; j<4; j++)
{
arr[i][j] = i*4+j;
}
}
for(i=0; i<3; i++)
{
int j = 0;
for(j=0; j<4; j++)
{
printf("%d ", arr[i][j]);
}
}
return 0;
}
仔细观察输出的结果,我们知道,随着数组下标的增长,元素的地址,也在有规律的递增。由此可以得出结论:数组在内存中是连续存放的。
#include <stdio.h>
int main()
{
int arr[10] = {0};
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; ++i)
{
printf("&arr[%d] = %p\n", i, &arr[i]);
}
return 0;
像一维数组一样,这里我们尝试打印二维数组的每个元素。
#include <stdio.h>
int main()
{
int arr[3][4];
int i = 0;
for(i=0; i<3; i++)
{
int j = 0;
for(j=0; j<4; j++)
{
printf("&arr[%d][%d] = %p\n", i, j,&arr[i][j]);
}
}
return 0;
}
数组的下标是有范围限制的。
数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。
C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就是正确的。
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int i = 0;
for(i=0; i<=10; i++)
{
printf("%d\n", arr[i]);//当i等于10的时候,越界访问了
}
return 0;
}
二维数组的行和列也可能存在越界。
在 C 语言中,数组名表示数组的地址,即数组首元素的地址。当我们在声明和定义一个数组时,该数组名就代表着该数组的地址。
例如,在以下代码中:
int myArray[5] = {10, 20, 30, 40, 50};
在这里,myArray 是数组名,它表示整数类型的数组,包含 5 个元素。myArray 也代表着数组的地址,即第一个元素的地址。
数组名本身是一个常量指针,意味着它的值是不能被改变的,一旦确定,就不能再指向其他地方。我们可以使用&运算符来获取数组的地址,如下所示:
int myArray[5] = {10, 20, 30, 40, 50};
int *ptr = &myArray[0];
// 或者直接写作 int *ptr = myArray;
在上面的例子中,*ptr指针变量被初始化为myArray的地址,即数组的第一个元素的地址。
需要注意的是,虽然数组名表示数组的地址,但在大多数情况下,数组名会自动转换为指向数组首元素的指针。这意味着我们可以直接将数组名用于指针运算,例如在函数传递参数或遍历数组时:
void printArray(int arr[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
// 数组名arr被当作指针使用
}
}
int main()
{
int myArray[5] = {10, 20, 30, 40, 50};
printArray(myArray, 5);
//将数组名传递给函数
return 0;
}
在上述代码中,printArray 函数接受一个整数数组和数组大小作为参数,我们将 myArray 数组名传递给函数,函数内部可以像使用指针一样使用 arr 数组名。
#define MAX 1000
对于#define ADD(x, y) ((x)+(y)),ADD是宏名,(x, y)是宏的参数,参数是无类型的,((x)+(y))是宏体.
宏是用来替换的.
宏是有参数的.
define定义宏
#define ADD(x, y) ((x)+(y))
#include <stdio.h>
int main()
{
int sum = ADD(2, 3);
printf("sum = %d\n", sum);
sum = 10*ADD(2, 3);
printf("sum = %d\n", sum);
return 0;
}
内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的.所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节.为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址.
给32位电脑的地址线通电,每一个地址线电信号都是1/0,32根地址线,一共可以产生232个序列,等于4GB
变量是创建内存中的(在内存中分配空间的),每个内存单元都有地址,所以变量也是有地址的。
&是取地址操作符
#include <stdio.h>
int main()
{
int num = 10;//向内存申请4个字节,存储10
#//取出num的地址
//注:这里num的4个字节,每个字节都有地址,取出的是第一个字节的地址(较小的地址)
printf("%p\n", &num);//打印地址,%p是以地址的形式打印
return 0;
}
在内存单元的编号->地址->地址也被称为是指针, 指针就是用来存放地址的.
存放指针(地址)的变量就是指针变量,指针变量分为全局变量和局部变量
局部变量在代码运行的时候才会指派地址,而全局变量在编译的时候就会分配地址.
&a取出的是a所占4个字节中地址较⼩的字节的地址。
示意:
int a = 10;
int* p = &a;//这就创建了一个指针变量
*指针变量示意图*
再举一例:以整形指针举例,可以推广到其他类型
char c = "abc";
char * pc = &c;
*p,记为解引用操作符,意思就是通过p中存放的地址,找到p所指的对象,*p就是p指向的对象.
可以用*p来代替p的对象,并可用于赋值操作.
#include <stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'q';//这里直接修改了pc所指向的对象
printf("%c\n", ch);
return 0;
}
不管是什么类型的指针,都是在创建指针变量,而指针变量是用来存放地址的.指针变量的大小取决于一个地址存放的时候需要多大空间,
32位平台下地址是32个bit位(即4个字节)
64位平台下地址是64个bit位(即8个字节)
#include <stdio.h>
int main()
{
printf("%d\n", sizeof(char *));
printf("%d\n", sizeof(short *));
printf("%d\n", sizeof(int *));
printf("%d\n", sizeof(double *));
return 0;
}
结果是:
8
8
8
8
对⽐,下⾯2段代码,主要在调试时观察内存的变化。
//代码1
#include <stdio.h>
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
//代码2
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
比如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。
先看⼀段代码,调试观察地址的变化。
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
结果是:
我们可以看出,char*类型的指针变量+1跳过1个字节,int*类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的+-整数和解引用的运算。
举例:
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
return 0;
}
在上⾯的代码中,将⼀个int类型的变量的地址赋值给⼀个char* 类型的指针变量。编译器给出了⼀个警告(如下图),是因为类型不兼容。⽽使⽤void*类型就不会有这样的问题。
使⽤void*类型的指针接收地址:
#include <stdio.h>
int main()
{
int a = 10;
void* pa = &a;
void* pc = &a;
*pa = 10;
*pc = 0;
return 0;
}
这⾥我们可以看到, void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算。
那么 void* 类型的指针到底有什么⽤呢?
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据,
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。
但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
return 0;
}
上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对n就⾏修改,就不符合语法规则,就报错,致使没法直接修改n。
但是如果我们绕过n,使⽤n的地址,去修改n就能做到了,虽然这样做是在打破语法规则。
#include <stdio.h>
int main()
{
const int n = 0;
printf("n = %d\n", n);
int*p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
输出结果:
n=0;
n=20;
⼀般来讲const修饰指针变量,可以放在* 的左边,也可以放在* 的右边,意义是不⼀样的。
int * p;//没有const修饰?
int const * p;//const 放在*的左边做修饰
int * const p;//const 放在*的右边做修饰
我们看下⾯代码,来分析具体分析⼀下:
#include <stdio.h>
//代码1 - 测试⽆const修饰的情况
void test1()
{
int n = 10;
int m = 20;
int *p = &n;
*p = 20;//ok? 可以
p = &m; //ok? 可以
}
//代码2 - 测试const放在*的左边情况
void test2()
{
int n = 10;
int m = 20;
const int* p = &n;//或者 int const* p = &n;等价
*p = 20;//ok? 不行
p = &m; //ok? 仍然可以
}
//代码3 - 测试const放在*的右边情况
void test3()
{
int n = 10;
int m = 20;
int * const p = &n;
*p = 20; //ok? 可以
p = &m; //ok? 不可以
}
//代码4 - 测试*的左右两边都有const
void test4()
{
int n = 10;
int m = 20;
int const * const p = &n;
*p = 20; //ok? 不行
p = &m; //ok? 不行
}
int main()
{
//测试⽆const修饰的情况
test1();
//测试const放在*的左边情况
test2();
//测试const放在*的右边情况
test3();
//测试*的左右两边都有const
test4();
return 0;
}
结论:const修饰指针变量的时候
- const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
- const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
但是指针变量本⾝的内容可变。
指针的基本运算有三种,分别是:
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
#include <stdio.h>
//指针+- 整数
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
}
return 0;
}
写一个函数使其达到strlen函数的效果
//方法一:使用库函数
#include<string.h>
int main()
{
int len = strlen("abcdef");
printf("%d\n",len);
return 0;
}
//方法二:
int my_strlen(char* str)
{
int count = 0;
while(*str !='\0')
{
count++;
str++;
}
return count;
}
int main()
{
int len =my_strlen("abcdef");
printf("%d\n",len);
return 0;
}
//方法三:递归版本
//方法四:指针-指针
#include <stdio.h>
int my_strlen(char* s)
{
char *p = s;
while(*p != '\0')
p++;
return p-s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
//指针的关系运算
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
*p++ 和 (*p)++的区别
*p++这个是指指针指向的位置为(比如某个位置),然后不操作,再然后p的值加上1(*p)++是指将*p这个指针所指的值加上1概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
初始化如下:
#include <stdio.h>
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;
return 0;
}
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。 我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起 来。 不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使⽤。
int main()
{
int arr[10] = {1,2,3,4,5,67,7,8,9,10};
int *p = &arr[0];//arr
for(i=0; i<10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使⽤的时候,判断p不为NULL的时候再使⽤
//...
p = &arr[0];//重新让p获得地址
if(p != NULL) //判断
{
//...
}
return 0;
}
如造成野指针的第3个例⼦,不要返回局部变量的地址。
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。
assert(p != NULL);
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误 流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语 句。
assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。
⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响⽤⼾使⽤时程序的效率。
库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数。 函数原型如下:
size_t strlen ( const char * str );
参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回⻓度。
如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直到 \0 就停⽌。
参考代码如下:
int my_strlen(const char * str)
{
int count = 0;
assert(str);
while(*str)
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n", len);
return 0;
}
学习指针的⽬的是使⽤指针解决问题,那什么问题,⾮指针不可呢?
例如:写⼀个函数,交换两个整型变量的值
⼀番思考后,我们可能写出这样的代码:
#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
我们发现在main函数内部,创建了a和b,a的地址是0x00cffdd0,b的地址是0x00cffdc4,在调⽤Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是x的地址是0x00cffcec,y的地址是0x00cffcf0,x和y确实接收到了a和b的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值,⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这种叫传值调⽤。
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。
所以Swap是失败的了。
那怎么办呢?
我们现在要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap 函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。
#include <stdio.h>
void Swap2(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤。
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。
有这样的代码:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
这⾥我们使⽤ &arr[0] 的⽅式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,⽽且是数组⾸元素的地址,我们来做个测试。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
我们发现数组名和数组⾸元素的地址打印出的结果⼀模⼀样,数组名就是数组⾸元素(第⼀个元素)的地址。
如何理解下面的代码:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));
return 0;
}
输出的结果是:40,如果arr是数组⾸元素的地址,那输出应该的应该是4/8才对。
其实数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:
除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。
再试⼀下这个代码:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
return 0;
}
三个打印结果⼀模⼀样,这时候⼜纳闷了,那arr和&arr有啥区别呢?
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0]+1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr+1);
return 0;
}
输出结果:
&arr[0] = 0077F820
&arr[0]+1 = 0077F824
arr = 0077F820
arr+1 = 0077F824
&arr = 0077F820
&arr+1 = 0077F848
这⾥我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是⾸元素的地址,+1就是跳过⼀个元素。但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。
总结:数组名是数组⾸元素的地址,但是有2个例外。
有了前⾯知识的⽀持,再结合数组的特点,我们就可以很⽅便的使⽤指针访问数组了。
#include <stdio.h>
int main()
{
int arr[10] = {0};
//输⼊
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
//输⼊
int* p = arr;
for(i=0; i<sz; i++)
{
scanf("%d", p+i);
//scanf("%d", arr+i);//也可以这样写
}
//输出
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
这个代码搞明⽩后,我们再试⼀下,如果我们再分析⼀下,数组名arr是数组⾸元素的地址,可以赋值给p,其实数组名arr和p在这⾥是等价的。那我们可以使⽤arr[i]可以访问数组的元素,那p[i]是否也可以访问数组呢?
#include <stdio.h>
int main()
{
int arr[10] = {0};
//输⼊
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
//输⼊
int* p = arr;
for(i=0; i<sz; i++)
{
scanf("%d", p+i);
//scanf("%d", arr+i);//也可以这样写
}
//输出
for(i=0; i<sz; i++)
{
printf("%d ", p[i]);
}
return 0;
}
将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i] 是等价于 *(p+i)。同理arr[i] 应该等价于 *(arr+i),数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的.
数组我们学过了,之前也讲了,数组是可以传递给函数的,这个⼩节我们讨论⼀下数组传参的本质。⾸先从⼀个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给⼀个函数后,函数内部求数组的元素个数吗?
#include <stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr)/sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int sz1 = sizeof(arr)/sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
输出结果:
sz=10;
sz2=1;
我们发现在函数内部是没有正确获得数组的元素个数。
这就要学习数组传参的本质了,上个⼩节我们学习了:数组名是数组⾸元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组⾸元素的地址。
所以函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。那么在函数内部我们写sizeof(arr) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是数组的⼤⼩(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
void test(int arr[])//参数写成数组形式,本质上还是指针
{
printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{
printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
test(arr);
return 0;
}
总结:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
void bubble_sort(int arr[], int sz)
//参数接收数组元素个数
{
int i = 0;
for(i=0; i<sz-1; i++)
{
int j = 0;
for(j=0; j<sz-i-1; j++)
{
if(arr[j] > arr[j+1])
{
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
int main()
{
int arr[] = {3,1,7,5,8,9,0,2,4,6};
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr,sz);
int i = 0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
return 0;
}
//⽅法2 - 优化
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
int i = 0;
for(i=0; i<sz-1; i++)
{
int flag = 1;//假设这⼀趟已经有序了
int j = 0;
for(j=0; j<sz-i-1; j++)
{
if(arr[j] > arr[j+1])
{
flag = 0;//发⽣交换就说明,⽆序
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
if(flag == 1)
//这⼀趟没交换就说明已经有序,后续⽆序排序了
break;
}
}
int main()
{
int arr[] = {3,1,7,5,8,9,0,2,4,6};
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr, sz);
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪⾥?
这就是 ⼆级指针 。
对于⼆级指针的运算有:
+ `*ppa`通过对ppa中的地址进⾏解引⽤这样找到的是pa,`*ppa`其实访问的就是pa
```c
int b = 20;
*ppa = &b;//等价于 pa = &b;
```
**ppa 先通过 *ppa 找到 pa ,然后对 pa 进⾏解引⽤操作: *pa,那找到的是a**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
指针数组是指针还是数组? 我们类⽐⼀下,整型数组,是存放整型的数组,字符数组是存放字符的数组。
那指针数组呢?是存放指针的数组。
指针数组的每个元素都是⽤来存放地址(指针)的。
指针数组的每个元素是地址,⼜可以指向⼀块区域。
#include <stdio.h>
int main()
{
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
int* parr[3] = {arr1, arr2, arr3};
int i = 0;
int j = 0;
for(i=0; i<3; i++)
{
for(j=0; j<5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。
上述的代码模拟出⼆维数组的效果,实际上并⾮完全是⼆维数组,因为每⼀⾏并⾮是连续的。
在指针的类型中我们知道有⼀种指针类型为字符指针 char* ; ⼀般使⽤:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
还有⼀种使⽤⽅式如下:
int main()
{
const char* pstr = "hello bit.";
//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
printf("%s\n", pstr);
return 0;
}
代码 const char* pstr = "hello bit."; 特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr ⾥了,但是本质是把字符串 hello bit. ⾸字符的地址放到了pstr中.

上⾯代码的意思是把⼀个常量字符串的⾸字符 h 的地址存放到指针变量 pstr 中。
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
if(str1 ==str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if(str3 ==str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
**这⾥str3和str4指向的是⼀个同⼀个常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域,当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。**但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
之前我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。
数组指针变量是指针变量?还是数组?
答案是:指针变量。
我们已经熟悉:
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
int *p1[10];
int (*p2)[10];
思考⼀下:p1, p2分别是什么?
数组指针变量
int (*p)[10];
解释:p先和结合,说明p是⼀个指针变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫*数组指针。
这⾥要注意:[]的优先级要⾼于号的,所以必须加上()来保证p先和结合。
数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是我们之前学习的 &数组名 。
#include <stdio.h>
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
可见数组名和&数组名打印的地址是一样的。
我们再看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr= %p\n", &arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr+1= %p\n", &arr+1);
return 0;
}
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。实际上:&arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
本例中&arr 的类型是:int(*)[10],是一种数组指针类型,数组的地址+1,跳过整个数组的大小,所以&arr+1 相对于&arr 的差值是40.
如果要存放个数组的地址,就得存放在数组指针变量中,如下:
int(*p)[10] = &arr;
我们调试也能看到 &arr 和 p 的类型是完全⼀致的。
数组指针类型解析:
int (*p) [10] = &arr;
| | |
| | |
| | p指向数组的元素个数
| p是数组指针变量名
p指向的数组的元素类型
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
int arr[5];//arr是整型数组
int *parr1[10];//parr1是指针数组
int (*parr2)[10];//parr2是数组指针
int (*parr3[10])[5];//parr3是存放数组指针的数组
有了数组指针的理解,我们就能够讲⼀下⼆维数组传参的本质了。 过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们是这样写的:
#include <stdio.h>
void test(int a[3][5], int r, int c)
{
int i = 0;
int j = 0;
for(i=0; i<r; i++)
{
for(j=0; j<c; j++)
{
printf("%d ", a[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};
test(arr, 3, 5);
return 0;
}
这⾥实参是⼆维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?
⾸先我们再次理解⼀下⼆维数组,⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。
如下图:
所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。如下:
#include <stdio.h>
void test(int (*p)[5], int r, int c)
{
int i = 0;
int j = 0;
for(i=0; i<r; i++)
{
for(j=0; j<c; j++)
{
printf("%d ", *(*(p+i)+j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};
test(arr, 3, 5);
return 0;
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
}
总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。
在写代码的时候难免把数组或者指针传给函数,那么函数的参数该如何设计呢?
#include <stdio.h>
void test(int arr[])//ok? 正确
{}
void test(int arr[10])//ok? 正确
{}
void test(int *arr)//ok? 正确
{}
void test2(int *arr[20])//ok? 正确
{}
void test2(int **arr)//ok? 正确
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}
void test(int arr[3][5])//ok? 可以
{}
void test(int arr[][])//ok? 不可以,形参的二维数组行{} 可以省略,列不能省略
void test(int arr[][5])//ok? 可以
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?
{} //不行,二维数组的数组名其实是首元素的地址,也就是第一行的地址,第一行是一个一维数组的地址
void test(int* arr[5])//ok? 不行,这个是指针数组
{}
void test(int (*arr)[5])//ok? 正确
{}
void test(int **arr)//ok? 不正确
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
#include <stdio.h>
void print(int *p, int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d\n",*(p+i));
}
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9};
int *p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
比如:
void test1(int *p)
{}
//test1函数能接收什么参数?
//int a =10;
//int* prt=&a;
//int arr[10];
//以下都可以接收
//print(&a);
//print(ptr);
//print(arr);
void test2(char* p)
{}
//test2函数能接收什么参数?
//char a ='w';
//char* ptr =&a;
//char arr[10];
//以下都可以接收
//print(&a);
//print(ptr);
//print(arr);
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
思考:
当函数的参数为二级指针的时候,可以接收什么参数?
void test(char **p)
{
}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);//Ok? 正确
return 0;
}
什么是函数指针变量呢?
根据前⾯学习整型指针,数组指针的时候,我们的类⽐关系,我们不难得出结论:
函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。
那么函数是否有地址呢? 我们做个测试:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
输出结果如下:
test: 005913CA
&test: 005913CA
确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的⽅式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针⾮常类似。如下:
void test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;
//x和y写上或者省略都是可以的
函数指针类型解析:
int (*pf3) (int x, int y)
| | ------------
| | |
| | pf3指向函数的参数类型和个数的交代
| 函数指针变量名
pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型
通过函数指针调⽤指针指向的函数。
#include <stdio.h>
int Add(int x, int y)
{
return x+y;
}
int main()
{
int(*pf3)(int, int) = Add;
printf("%d\n", (*pf3)(2, 3));
printf("%d\n", pf3(3, 5));//这样写也完全可行
return 0;
}
输出结果:
5
8
代码1:
(*(void (*)())0)();
这是一次函数调用;先是将整数0强制类型转换为:无参,返回类型是void的函数的地址,调用0地址处的这个函数。
代码2:
void (*signal(int , void(*)(int)))(int);
//申明的signal函数的第一个参数的类型是int,第二个参数是一个函数指针,这个函数指针指向的函数参数是int,返回类型是void,signal函数的返回类型也是一个函数指针,该函数指针指向的函数参数是int,返回类型是void。
两段代码均出⾃:《C陷阱和缺陷》这本书
typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。
⽐如,你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使⽤:
typedef unsigned int uint;
如果是指针类型,能否重命名呢?其实也是可以的,⽐如,将 int* 重命名为 ptr_t ,这样写:
typedef int* ptr_t;
但是对于数组指针和函数指针稍微有点区别:
⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
函数指针类型的重命名也是⼀样的,⽐如,将 void(*)(int) 类型重命名为 pf_t ,就可以这样写:
typedef void(*pfun_t)(int);//新的类型名必须在*的右边
那么要简化代码2,可以这样写:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组,⽐如:
int *arr[10];
//数组的每个元素是int*
那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
答案是:parr1
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。
函数指针数组的⽤途:转移表
举例:计算器的⼀般实现:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
void menu()
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf("请选择:");
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
使⽤函数指针数组的实现:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a / b;
}
void menu()
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf("请选择:");
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int (*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
do
{
menu();
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf( "输⼊操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);
printf( "ret = %d\n", ret);
}
else if(input == 0)
{
printf("退出计算器\n");
}
else
{
printf("选择错误\n");
}
}while (input);
return 0;
}
指向函数指针数组的指针是一个 指针 指针指向一个 数组 ,数组的元素都是 函数指针 ; 如何定义?
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
#include <stdlib>
使用快速排序的思想实现的一个排序函数。 qsort可以排序任意类型的数据。
void qsort( void *base, //要排序的数据的起始位置
size_t num, //待排序的数据元素的个数
size_t width, //待排序的数据元素的大小(单位:byte)
int (*cmp )(const void *e1, const void *e2) //函数指针-比较函数
);
#include <stdio.h>
//qosrt函数的使用者得先拿出一个对其想要排序的数据的排序规则的实现函数
int int_cmp(const void * p1, const void * p2)
{
return (*( int *)p1 - *(int *) p2);
//这里提供了一个好思路,通过相减的方式来比较两个数字的大小
}
//主函数
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i<sz ; i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
struct Stu //学⽣
{
char name[20];//名字
int age;//年龄
};
//假设按照年龄来⽐较
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
//假设按照名字来⽐较
//strcmp - 是库函数,是专⻔⽤来⽐较两个字符串的⼤⼩的
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
//按照年龄来排序
void test2()
{
struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
//按照名字来排序
void test3()
{
struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{
test2();
test3();
return 0;
}
使⽤回调函数,模拟实现qsort(采⽤冒泡的⽅式)。
#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{
return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size)
{
int i = 0;
for (i = 0; i< size; i++)
{
char tmp = *((char *)p1 + i);
*(( char *)p1 + i) = *((char *) p2 + i);
*(( char *)p2 + i) = tmp;
}
}
void bubble(void *base, int count , int size, int(*cmp )(void *e1, void *e2))
{
int i = 0;
int j = 0;
for (i = 0; i< count - 1; i++)
{
for (j = 0; j<count-i-1; j++)
{
if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size)>0)
{
_swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
}
}
}
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
//char *arr[] = {"aaaa","dddd","cccc","bbbb"};
int i = 0;
bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
在学习操作符的时候,我们学习了 sizeof , sizeof 计算变量所占内存内存空间⼤⼩的,单位是字节,如果操作数是类型的话,计算的是使⽤类型创建的变量所占内存空间的⼤⼩。
sizeof 只关注占⽤内存空间的⼤⼩,不在乎内存中存放什么数据。
比如:
#inculde <stdio.h>
int main()
{
int a = 10;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof a);
printf("%d\n", sizeof(int));
return 0;
}
strlen 是C语⾔库函数,功能是求字符串⻓度。函数原型如下:
size_t strlen ( const char * str );
统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。 strlen 函数会⼀直向后找 \0 字符,直到找到为止,所以可能存在越界查找。
#include <stdio.h>
int main()
{
char arr1[3] = {'a', 'b', 'c'};
char arr2[] = "abc";
printf("%d\n", strlen(arr1));
printf("%d\n", strlen(arr2));
printf("%d\n", sizeof(arr1));
printf("%d\n", sizeof(arr2));
return 0;
}
sizeof()
strlen()
string.h\0之前字符的隔个数\0,如果没有\0,就会持续往后找,可能会越界考察对数组名的理解和指针的运算和指针类型的意义
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a)); //16
//sizeof(数组名),数组名表示整个数组,计算的是整个数组的大小,单位是字节
printf("%d\n",sizeof(a+0)); // 4或8
//a不是单独放在sizeof内部,也没有取地址,a+0表示数组首元素的地址
printf("%d\n",sizeof(*a)); //4
//*a相当于a[0]
printf("%d\n",sizeof(a+1)); // 4或8
//这里的a是数组首元素的地址,a+1是第二个元素的地址
//sizeof(a+1)就是地址的大小
printf("%d\n",sizeof(a[1])); //4
//计算的是第二个元素的大小
printf("%d\n",sizeof(&a)); // 4或8
//&a取出的数组的地址,数组的地址,也是一个地址
printf("%d\n",sizeof(*&a)); //16
//&a---->int(*)[4]
//&a拿到的是数组名的地址,类型是int(*)[4],是一种数组指针
//数组指针解引用找到的是数组
//*&a--->a
//相当于sizeof(a)
printf("%d\n",sizeof(&a+1)); // 4或8
//&a取出的是数组的地址
//&a--> int(*)[4]
//&a+1是从数组a的地址向后跳过了一个(4个整型元素的)数组的大小
//&a+1还是地址,是地址就是4/8字节
printf("%d\n",sizeof(&a[0])); // 4或8
//&a[0]就是第一个元素的地址
//计算的是地址的大小
printf("%d\n",sizeof(&a[0]+1)); // 4或8
//&a[0]+1是第二个元素的地址
//大小是4/8个字节
//&a[0]+1--->&a[1]
代码1:
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr)); //6
printf("%d\n", sizeof(arr+0)); //4或8
printf("%d\n", sizeof(*arr)); //1
//*arr相当于*(arr+0),相当于arr[0];
printf("%d\n", sizeof(arr[1])); //1
//arr[1]相当于*(arr+1)
printf("%d\n", sizeof(&arr)); //4或8
printf("%d\n", sizeof(&arr+1)); //4或8
printf("%d\n", sizeof(&arr[0]+1)); //4或8
代码2:
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr)); //不确定(随机值)
printf("%d\n", strlen(arr+0)); //不确定(随机值)
printf("%d\n", strlen(*arr)); //程序出错
//相当于strlen('a');--->strlen(97),野指针
printf("%d\n", strlen(arr[1])); //程序出错
//理由同上
printf("%d\n", strlen(&arr)); //不确定(随机值)和arr一样
printf("%d\n", strlen(&arr+1)); //不确定(随机值),比arr少了6
printf("%d\n", strlen(&arr[0]+1)); //不确定(随机值),比arr少了1
代码3:
char arr[] = "abcdef";
printf("%d\n", sizeof(arr)); //7
printf("%d\n", sizeof(arr+0)); //4或8
printf("%d\n", sizeof(*arr)); //1
printf("%d\n", sizeof(arr[1])); //1
printf("%d\n", sizeof(&arr)); //4或8
printf("%d\n", sizeof(&arr+1)); //4或8
printf("%d\n", sizeof(&arr[0]+1)); //4或8
代码4:
char arr[] = "abcdef";
printf("%d\n", strlen(arr)); //6
printf("%d\n", strlen(arr+0)); //6
printf("%d\n", strlen(*arr)); //出问题
printf("%d\n", strlen(arr[1])); //出问题
printf("%d\n", strlen(&arr)); //6
printf("%d\n", strlen(&arr+1)); //不确定(随机值)
printf("%d\n", strlen(&arr[0]+1)); //5
代码5:
char *p = "abcdef";
printf("%d\n", sizeof(p)); //4或8
printf("%d\n", sizeof(p+1)); //4或8
printf("%d\n", sizeof(*p)); //1
printf("%d\n", sizeof(p[0])); //1
//p[0]-->*(p+0)-->*p
printf("%d\n", sizeof(&p)); //4或8
printf("%d\n", sizeof(&p+1)); //4或8
printf("%d\n", sizeof(&p[0]+1)); //4或8
代码6
char *p = "abcdef";
printf("%d\n", strlen(p)); //6
printf("%d\n", strlen(p+1)); //5
printf("%d\n", strlen(*p)); //出问题
printf("%d\n", strlen(p[0])); //出问题
printf("%d\n", strlen(&p)); //随机值
printf("%d\n", strlen(&p+1)); //随机值(和&p无必然联系)
printf("%d\n", strlen(&p[0]+1)); //5
int a[3][4] = {0};
printf("%d\n",sizeof(a)); //48
printf("%d\n",sizeof(a[0][0])); //4
printf("%d\n",sizeof(a[0])); //16
//a[0]是第一行这个一维数组的数组名,单独放在sizeof内部,
//a[0]表示第一个整个这个一维数组
printf("%d\n",sizeof(a[0]+1)); //4或8
//a[0]并没有单独放在sizeof内部,也没有取地址,a[0]就表示首元素的地址,就是第一行这个一维数组的第一个元素的地址
//a[0]+1就是第一行第二个元素的地址
printf("%d\n",sizeof(*(a[0]+1))); //4
printf("%d\n",sizeof(a+1)); //4或8
//a虽然是二维数组的地址,但是并没有单独放在sizeof内部,也没取地址。
//a表示首元素的地址,二维数组的首元素是它的第一行
//a就表示第一行的地址,a+1就是跳过第一行,表示第二行的地址
printf("%d\n",sizeof(*(a+1))); // 16
//*(a+1)-->a[1],相当于sizeof(a[1])
printf("%d\n",sizeof(&a[0]+1)); // 4或8
//&a[0]--->&*(a+0)-->a,所以&a[0]+1--->第二行的地址
printf("%d\n",sizeof(*(&a[0]+1))); //16
printf("%d\n",sizeof(*a)); //16
printf("%d\n",sizeof(a[3])); //16
数组名的意义:
如果一个数组名,既没有单独放在sizeof内部,也没有取地址,则这里的数组名表示首元素的地址
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
//程序的结果是什么? 2,5
//在X86环境下
//假设结构体的大小是20个字节
//程序输出的结果是啥?
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{
printf("%p\n", p + 0x1);
//结构体指针+1相当于地址加上指针的大小
//0x100000+20-->0x100014
printf("%p\n", (unsigned long)p + 0x1);
//0x100001
printf("%p\n", (unsigned int*)p + 0x1);
//0x100004
return 0;
}
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf( "%d", p[0]);
return 0;
}
//假设环境是x86环境,程序输出的结果是啥?
#include <stdio.h>
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
#include <stdio.h>
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *ptr1 = (int *)(&aa + 1);
int *ptr2 = (int *)(*(aa + 1));
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
输出:10,
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
#include <stdio.h>
int main()
{
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char **cp[] = {c+3,c+2,c+1,c};
char ***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp+3);
printf("%s\n", *cpp[-2]+3);
printf("%s\n", cpp[-1][-1]+1);
return 0;
}
int main()
{
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf( "%x,%x", ptr1[-1], *ptr2);
return 0;
}
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
而数组是一组相同类型的元素集合,复杂对象的描述需要使用结构体
struct tag
{
member-list;
}(variable-list);
例如描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[6];//性别
char id[20];//学号
}p1,p2;//分号不能丢
这里的p1和p2就是通过Stu这个结构体创建的两个变量,注意它们是全局的结构体变量。
结构的成员可以是标量、数组、指针,甚至是其他结构体。
有了结构体类型,那如何定义变量,其实很简单。
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point
{
int x;
int y;
}
int main()
{
struct Point p2,p3,p4;
//定义结构体变量p2,这些都是全局的结构体变量
struct Point p3 = {x, y};
//初始化:定义变量的同时赋初值。
}
struct Stu //类型声明
{
char name[15]; //名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL};
//结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};
//结构体嵌套初始化
在声明结构的时候,可以不完全的声明。
比如:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
上面的两个结构在声明的时候省略掉了结构体标签(tag)。
那么问题来了?
//在上面代码的基础上,下面的代码合法吗?
p = &x;
警告:
编译器会把上面的两个声明当成完全不同的两个类型。
所以是非法的
在结构中包含一个类型为该结构本身的成员是否可以呢?
//代码1
struct Node
{
int data;
struct Node next;
};
//可行否?
如果可以,那sizeof(struct Node)是多少?
正确的自引用方式:
//代码2
struct Node
{
int data;
struct Node* next;
};
注意:
//代码3
typedef struct
{
int data;
Node* next;
}Node;
//这样写代码,可行否?
//解决方案:
typedef struct Node
{
int data;
struct Node* next;
}Node;
struct Stu s;
strcpy(s.name, "zhangsan");//使用.访问name成员
s.age = 20;//使用.访问age成员
struct Stu
{
char name[20];
int age;
};
void print(struct Stu* ps)
{
printf("name = %s age = %d\n", (*ps).name, (*ps).age);
//使用结构体指针访问指向对象的成员
printf("name = %s age = %d\n", ps->name, ps->age);
}
int main()
{
struct Stu s = {"zhangsan", 20};
print(&s);
//结构体地址传参
return 0;
}
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};//创建结构体变量
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
上面的 print1 和 print2 函数哪个好些? 答案是:首选print2函数。
函数传参的时候,参数是需要压栈的。 如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。
举例:
//step1:打印结构体信息(此时并不占用内存)
struct Stu
{
//结构成员
char name[20];//数组
int age;
char sex[10];//数组
char phoneNum[12];//数组
}; //这个";"不可少,用于隔开
int main()
{
struct Stu s = {"zhangsan",20,"nan","123456"};
//step2:初始化结构体
printf("%s %d %s %s\n",s.name, s.age, s.sex, s.phoneNum);
//'.'为结构成员访问操作符
//使用方法:结构体对象.成员名
}
以上代码可以用指针替换实现,
struct Stu
{
char name[20];
int age;
char sex[10];
char phoneNum[12];
};
void print(struct Stu* ps)
{
printf("%s %d %s %s\n",(*ps).name, (*ps).age, (*ps).sex, (*ps).phoneNum);
printf("%s %d %s %s\n",ps->name, ps->age, ps->sex, ps->phoneNum);
//用法:结构体指针变量->成员名
}
int main()
{
struct Stu s = {"zhangsan",20,"nan","123456"};
print(&s);
}
当我们发现程序中存在的问题的时候,那下一步就是找到问题,并修复问题。这个找问题的过程叫称为调试,英文叫debug(消灭bug)的意思。
调试一个程序,首先是承认出现了问题,然后通过各种手段去定位问题的位置,可能是逐过程的调试,也可能是隔离和屏蔽代码的方式,找到问题所的位置,然后确定错误产生的原因,再修复代码,重新测试。
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序;程序员在写代码的时候,需要经常性的调试代码,就将这里设置为debug ,这样编译产生的是debug 版本的可执行程序,其中包含调试信息,是可以直接调试的。
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。当程序员写完代码,测试再对程序进行测试,直到程序的质量符合交付给用户使用的标准,这个时候就会设置为release ,编译产生的就是release 版本的可执行程序,这个版本是用户使用的,无需包含调试信息等。
对比可以看到从同一段代码,编译生成的可执行文件的大小,release版本明显要小,而debug版本明显大。
首先是环境的准备,需要一个支持调试的开发环境,我们上课使用VS,应该把VS上设置为Debug。
F9:创建断点和取消断点
断点的作用是可以在程序的任意位置设置断点,打上断点就可以使得程序执行到想要的位置暂定执行,接下来我们就可以使用F10,F11这些快捷键,观察代码的执行细节。
条件断点:满足这个条件,才触发断点
F5:启动调试,经常用来直接跳到下一个断点处,一般是 和F9配合使用。
F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部。在函数调用的地方,想进入函数观察细节,必须使用F11,如果使用F10,直接完成函数调用。
CTRL + F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
在调试的过程中我们,如果要观察代码执行过程中,上下文环境中的变量的值,有哪些方法呢?
这些观察的前提条件一定是开始调试后观察,比如:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int num = 100;
char c = 'w';
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = i;
}
return 0;
}
开始调试后,在菜单栏中【调试】->【窗口】->【监视】,打开任意一个监视窗口,输入想要观察的对象就行。
打开监视窗口,并在监视窗口中观察。
如果监视窗口看的不够仔细,也是可以观察变量在内存中的存储情况,还是在【调试】->【窗口】->【内存】
打开内存窗口,在打开内存窗口后,要在地址栏输入:arr,&num,&c,这类地址,就能观察到该地址处的数据。除此之外,在调试的窗口中还有:自动窗口,局部变量,反汇编、寄存器等窗口。
求1!+2!+3!+4!+...10! 的和,请看下面的代码:
#include <stdio.h>
//写一个代码求n的阶乘
int main()
{
int n = 0;
scanf("%d", &n);
int i = 1;
int ret = 1;
for(i=1; i<=n; i++)
{
ret *= i;
}
printf("%d\n", ret);
return 0;
}
如果n分别是1,2,3,4,5...10,求出每个数的阶乘,再求和就好了,在上面的代码上改造:
int main()
{
int n = 0;
int i = 1;
int sum = 0;
for(n=1; n<=10; n++)
{
for(i=1; i<=n; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
//运行结果应该是错的?
调试找一下问题。ret未置1
在VS2022、X86、Debug 的环境下,编译器不做任何优化的话,下面代码执行的结果是啥?
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
for(i=0; i<=12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
程序运行,死循环了,调试看看为什么?
调试可以上面程序的内存布局如下:
知识点:
如果是上面的内存布局,那随着数组下标的增长,往后越界就有可能覆盖到i,这样就可能造成死循环的。
这里肯定有同学有疑问:为什么i和arr数组之间恰好空出来2个整型的空间呢?这里确实是巧合,在不同的编译器下可能中间的空出的空间大小是不一样的,代码中这些变量内存的分配和地址分配是编译器指定的,所以的不同的编译器之间就有差异了。所以这个题目是和环境相关的。
注意:栈区的默认的使用习惯是先使用高地址,再使用低地址的空间,但是这个具体还是要编译器的实现.
比如:
在VS上切换到X64,这个使用的顺序就是相反的,在Release版本的程序中,这个使用的顺序也是相反的。
如果一个代码稍微复杂,那怎么调试呢?
这里我们就上手调试一下扫雷的代码。
演示:
调试过程中,要做到心中有数,也就是程序员自己心里要清晰的知道希望代码怎么执行,然后再去看代码有没有按照我们预定的路线在执行。 调试是需要反复去动手练习的,调试是可以增加程序员对代码的理解和掌控的,掌握了调试的能力,就能看到本质,就像能给程序做B超一样,对程序内部一览无余。
直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。(不符合语法规则)
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
结果不是程序员想要的。
借助调试,逐步定位问题。最难搞。
常见的coding技巧:
/***
*char *strcpy(dst, src) - copy one string over another
*
*Purpose:
* Copies the string src into the spot specified by
* dest; assumes enough room.
*
*Entry:
* char * dst - string over which "src" is to be copied
* const char * src - string to be copied over "dst"
*
*Exit:
* The address of "dst"
*
*Exceptions:
*******************************************************************************/
char * strcpy(char * dst, const char * src)
{
char * cp = dst;
assert(dst && src);
while( *cp++ = *src++ )
; /* Copy src over dst */
return( dst );
}
char -1//字符数据类型
short -2 //短整型
int -4 //整型
long -4 //长整型
long long -8//更长的整型
float -4//单精度浮点数
double -8//双精度浮点数
long double -16
以及他们所占存储空间的大小。
类型的意义:
整形家族
char//字符的本质是ASCLL码值
char//标准未定义
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long [int]
signed long [int]
浮点数家族
float
double
构造类型
数组类型
结构体类型 struct
枚举类型 enum
联合类型 union
指针类型
int *pi;
char *pc;
float* pf;
void* pv;
空类型
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。
一个数在计算机中的表示形式是二进制的话,这个数其实就叫机器数。
机器数通常是带有符号的(指有正数和负数之分),计算机用最高位存放符号,这个 bit 一般叫做符号位。 正数的符号位为 0, 负数的符号位为 1。比如,十进制中的数 +7 ,计算机字长为8位,转换成二进制就是 0 0 0 0 0 1 1 1(一个 byte 有 8bit,有效的取值范围是 -128 ~ +127)。
如果是 -7 ,就是 1 0 0 0 0 1 1 1 。一个存储的二进制码分原码、反码、补码。
十进制数据的二进制表现形式就是原码,原码最左边的一个数字就是符号位,0为正,1为负。
例如:56 -> 0 0 1 1 1 0 0 0
左边第一位为符号位,其他位为数据位。
一个 byte 有 8bit,最大值是 0 1 1 1 1 1 1 1 (+127),最小值是 1 1 1 1 1 1 1 1 (-128)
在计算机中之所以使用二进制来表示原码是因为逻辑简单,对于电路来说只有开或者关两种状态,用二进制是在方便不过的了。如果使用的进制是十进制、八进制或者十六进制的话,电路没有办法表示那么多的状态。
使用原码对正数进行计算不会有任何问题的
例如:5+2
0 0 0 0 0 1 0 1
+ 0 0 1 0
-----------------
0 0 0 0 0 1 1 1
把这个结果转成十进制就等于7
但是如果是负数的话,那计算的结果就会大相径庭了
我们拿 -56 这个数字来举例,它的原码是 1 0 1 1 1 0 0 0 ,减一之后,就会变成 1 0 1 1 0 1 1 1 ,这个数转成十进制就是 -55。计算前是 -56,减一之后正确的结果应该是 -57(1 0 1 1 1 0 0 1)才对,居然还越减越大了。
1 0 1 1 1 0 0 0
- 1
-----------------
1 0 1 1 0 1 1 1
为了解决原码不能用于计算负数的这种问题,这时候,反码它出现了,作为负数的“计算的救星”。
计算规则是正数的反码不变和原码一致,负数的反码会在原码的基础上,高位的符号位不变,其他位取反( 1 变成 0 , 0 变为 1 )。
正数的反码是其本身(等于原码),负数的反码是符号位保持不变,其余位取反。
反码的存在是为了正确计算负数,因为原码不能用于计算负数。
-56 的原码是 1 0 1 1 1 0 0 0 ,如果转成反码(符号位不变,其他位取反),
那么它的反码就是 1 1 0 0 0 1 1 1
不过反码也有它的 “ 软肋 ”,如果是负数跨零进行计算的话,计算得出的结果不对
我们拿 -3 + 5 来举例
-3 的原码是 1 0 0 0 0 0 1 1,转成反码的话就是 1 1 1 1 1 1 0 0
1 1 1 1 1 1 0 0
+ 0 1 0 1
-----------------
0 0 0 0 0 0 0 1
把计算结果转成十进制就是 1,这结果显然不对。那么我们该怎么计算呢,这时候,作为反码的补充编码 —— 补码就出现了。
正数的补码是其本身,负数的补码等于其反码 +1。因为反码不能解决负数跨零(类似于 -6 + 7)的问题,所以补码出现了。
这时候,我们再来使用补码计算一下 -3 + 5 的结果
-3 的原码是 1 0 0 0 0 0 1 1,转成反码的话就是 1 1 1 1 1 1 0 0,再转成补码就是 1 1 1 1 1 1 0 1
1 1 1 1 1 1 0 1
+ 0 1 0 1
-----------------
0 0 0 0 0 0 1 0
把这个数转成十进制刚好等于2,结果正确
在计算机当中都是使用补码来进行计算和存储的。补码很好的解决了反码负数不能跨零计算的弊端,并且补码还可以记录一个特殊的值 -128,这个数据在 1 个字节下是没有原码和反码。
学习了原码、反码和补码的知识之后,我们就可以了解到,Java 当中所有的基本数据类型。比如整数类型的数据类型,存储的数都是同样的,区别是在于什么地方,假设存储的值都是 10。
public class Test {
public static void main(String[] args) {
// 小的数据类型往大的数据类型进行转换底层就是通过左补零完成的
byte a = 10; // 0000 1010
int b = a; // 0000 0000 0000 0000 0000 0000 0000 1010
System.out.println(b);
}
}
public class Test {
public static void main(String[] args) {
int a = 300;
// 0000 0000 0000 0000 0000 0001 0010 1100
byte b = (byte) a;
// 0010 1100
System.out.println(b); // 打印出44
/*
int a = 200;
// 0000 0000 0000 0000 0000 0000 1100 1000
byte b = (byte)a; // 1100 1000
System.out.println(b);// 打印出-56
*/
}
}
正数:
正整数的原码、反码和补码是一样的。
即看到符号位(第一位)是0,就可以照着写出其他两种码。
负数:
对于整形来说:数据存放内存中其实存放的是补码。
为什么呢?
在计算机系统中,数值一律用补码来表示和存储。 原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
当我们了解了整数在内存中存储后,我们调试看一个细节:
#include <stdio.h>
int main()
{
int a = 0x11223344;
return 0;
}
调试的时候,我们可以看到在a中的0x11223344 这个数字是按照字节为单位,倒着存储的。这是为什么呢?
其实超过一个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为大端字节序存储和小端字节序存储,下面是具体的概念:
大端(存储)模式:
是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处。
小端(存储)模式:
是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处。
上述概念需要记住,方便分辨大小端。
为什么会有大小端模式之分呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit 位,但是在C语言中除了8 bit 的char 之外,还有16 bit 的short 型,32 bit 的long 型(要看 具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
例如:一个16bit 的short 型x ,在内存中的地址为0x0010 , x 的值为0x1122 ,那么0x11 为高字节, 0x22 为低字节。对于大端模式,就将0x11 放在低地址中,即0x0010 中,0x22 放在高地址中,即0x0011 中。小端模式,刚好相反。我们常用的X86 结构是小端模式,而KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)-百度笔试题
//代码1
#include <stdio.h>
int check_sys()
{
int i = 1;
return (*(char *)&i);
}
int main()
{
int ret = check_sys();
if(ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
//代码2
int check_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;
}
#include <stdio.h>
int main()
{
char a= -1;
signed char b=-1;
unsigned char c=-1;
printf("a=%d,b=%d,c=%d",a,b,c);
return 0;
}
输出结果为: -1,-1,255
截断 将占用字节数较多的变量赋值给占用字节数较少的变量时,如将long(16个字节)赋值给int(4个字节)时,这时候long类型的比特位,只将最低的8位赋给了char类型的变量,而高位比特位全部被“截断”;
整型提升 整型提升 是 C程序设计语言 中的一项规定:在表达式计算时,各种整型首先要提升为int类型,如果int类型不足以表示则要提升为unsigned int类型;然后执行表达式的运算。
高数据类型向低数据类型转化时会发生截断;
低数据类型向高数据类型转化unsigned优先【如图】,或者进行运算时不满足int时会发生整形提升;
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
int i= -20;
unsigned int j = 10;
printf("%d\n", i+j);
//按照补码的形式进行运算,最后格式化成为有符号整数
unsigned int i;
for(i = 9; i >= 0; i--)
{
printf("%u\n",i);
}
int main()
{
char a[1000];
int i;
for(i=0; i<1000; i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0;
}
#include <stdio.h>
unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
printf("hello world\n");
}
return 0;
}
常见的浮点数:3.14159、1E10等,浮点数家族包括: float、double、long double 类型。
浮点数表示的范围:float.h 中定义
#include <stdio.h>
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
输出什么?
上面的代码中, num 和*pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。 根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
V = (−1)S ∗M ∗ 2E
• (−1)S 表示符号位,当S=0,V为正数;当S=1,V为负数 • M 表示有效数字,M是大于等于1,小于2的 • 2E 表示指数位
举例来说: 十进制的5.0,写成二进制是101.0 ,相当于1.01×2^2 。
那么,按照上面V的格式,可以得出S=0,M=1.01,E=2。 十进制的-5.0,写成二进制是-101.0 ,相当于-1.01×2^2 。那么,S=1,M=1.01,E=2。
IEEE 754规定:
IEEE 754 对有效数字M和指数E,还有一些特别规定。
前面说过, 1≤M<2 ,也就是说,M可以写成1.xxxxxx 的形式,其中xxxxxx 表示小数部分。IEEE 754 规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的
xxxxxx部分。
比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保 存24位有效数字。
至于指数E,情况就比较复杂.
首先,E为一个无符号整数(unsigned int)
这意味着,如果E为8位,它的取值范围为0255;如果E为11位,它的取值范围为02047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
指数E从内存中取出还可以再分成三种情况:
E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。 比如:0.5 的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1+127(中间值)=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为:
0 01111110 00000000000000000000000
E全为0 这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
0 00000000 00100000000000000000000
E全为1 这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);
0 11111111 00010000000000000000000
好了,关于浮点数的表示规则,就说到这里。
下面,让我们回到一开始的练习
先看第1环节,为什么9 还原成浮点数,就成了0.000000 ?
9以整型的形式存储在内存中,得到如下二进制序列:
0000 0000 0000 0000 1 0000 0000 0000 1001
首先,将9 的二进制序列按照浮点数的形式拆分,得到第一位符号位s=0,后面8位的指数E=00000000 , 最后23位的有效数字M=000 0000 0000 0000 0000 1001。 由于指数E全为0,所以符合E为全0的情况。因此,浮点数V就写成:
V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)
显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。再看第2环节,浮点数9.0,为什么整数打印是1091567616首先,浮点数9.0 等于二进制的1001.0,即换算成科学计数法是:1.001×2^3所以: 9.0 = (−1)0 ∗ (1.001) ∗ 23 ,那么,第一位的符号位S=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130,即10000010.所以,写成二进制形式,应该是S+E+M,即
0 10000010 001 0000 0000 0000 0000 0000
这个32位的二进制数,被当做整数来解析的时候,就是整数在内存中的补码,原码正是1091567616。
局部变量是怎么创建的? 为什么局部变量的值是随机值? 函数是怎么传参的?传参的顺序怎么样? 形参和实参是什么关系? 函数调用是怎么做的? 函数调用结束后是怎么返回的?
在学习函数栈帧前先要进行一点知识铺垫,这样会有助于我们后面对函数栈帧的理解
函数栈帧是在内存中的栈区为被调函数开辟的一块空间,里面用来存放该函数中定义的变量等东西(下文会详细讲到),当函数运行完毕栈帧将被销毁。再向大家介绍一下“栈”这个概念,“栈”实际上时一种数据结构,它是一种先进后出的数据表,对栈常见的操作有两种:
Push(入栈):为栈增加一个元素,就相当于往水池里放盘子
Pop (出栈): 从栈中取出一个元素,相当于洗完一个盘子把这个洗过的盘子从水池中拿出来
所有的函数调用都会在内存里面的栈区创建函数栈帧,包括main函数。通过上面对函数栈帧的介绍我们知道,函数栈帧是为被调函数在内存的栈区中开辟的一块空间,所以这里间接证明了,main函数也是被调函数。可能很多小伙伴的认知都停留在,main函数是主函数,可以在main函数中调用其他函数,从来没有想过main函数其实也是被调用的。
首先点击F10进行调试,在窗口界面找到“调用堆栈”,点击,调出此窗口,此时我们就能很直观的看出main函数是被调用的。
调试结束后我们发现main 函数被__tmainCRTStartup这个函数调用。
第一步:push ebp
第二步:move ebp,esp
第三步:sub esp,0E4h
第四步:3个push
第五步:lea加载有效地址
第六步:把main函数里面的空间全初始化
第七步:main函数中变量的创建
1:变量a的创建
2:变量b的创建
3:变量c的创建
由于main函数是被其他函数所调用,所以在 __tmainCRTStartup 这个函数调用main函数的时候会为main函数在内存的栈区中开辟空间:
栈区的使用习惯是先使用高地址再使用低地址,在顶上往进放数据
通过上图可以看出第一步是push ebp,这是因为mian函数是被__tmainCRTStartup 这个函数调用的,在调用main函数之前,esp和ebp分别指向__tmainCRTStartup 函数的栈顶和栈底,当调用main函数的时候,就要为main函数开辟相应的函数栈帧,此时esp和ebp就需要移动去指向main函数的函数栈帧。那这里的第一步就是push ebp,
push ebp就是把__mainCRTStartup 函数栈底的地址压栈,ebp的值压入后,esp指针会上移一位。
如上图,再push ebp没有执行的时候,esp里面存的地址是0x0037fdb8,当执行完push ebp后,esp存的地址变成了0x0037fdb4,可见地址减小了4,这就意味着esp指针往上走了4个字节。通过内存窗口我们也可以很容易看出:
此时esp所指向的地址里面存的数据就是ebp所指向的地址0037fe04,说明此时我们已经成功地把ebp所指向的地址压入栈中。
move ebp,esp的意思是:把esp的值给ebp。
esp当前的值是0x0037fdb4,也就是说esp此时指向0x0037fdb4这个地址,把esp的值给ebp后,ebp就也指向0x0037fdb4这个地址
sub esp,0ECh,就是给esp减去一个0E4h。这里的0E4h是一个十六进制的数字(h表示是十六进制),0E4对应的10进制数字就是228。这也就意味着esp指向的地址会减小228,对应图示就是esp指针会上移228个字节。
多出来的这一块空间就是为main函数申请的空间
执行完push ebx后,esp指向的地址减小了4个字节(从0037FCD0变到00F37CCC,前者比后者大4)。此时esp所指向的地址里面存储的就是ebx的值005c200,这说明ebx成功压栈。剩下的两个push esi和push edi也是这样入栈,
lea是load effective address(加载有效地址)的缩写。而 lea edi,[ebp-0E4h]的意思就是把ebp-0E4h这个地址放到edi里面。还记得第二步move ebp,esp嘛?。执行完第二步后ebp和esp指向了同一个地址,然后第三步sub esp,0E4h,让esp指向的地址减了0E4h(228),,此后ebp指向的地址没有发生任何变化,第四步的3个push操作让esp指向的地址又减小了12(一次push减小4,3次push就减小12)。而当前的第五步中的地址ebp-0E4h也就是在执行完第三步后esp所指向的地址,就是要把这个地址放到edi里面(其实就是让edi指向这个地址,因为edi是一个变址寄存器,用存储地址的)如下图:
执行完lea后,我们来看下面这三行汇编指令,这三行汇编指令放在一起只为了执行一件事情,所以把这三条指令放在一起看。(分别标上序号1 , 2 ,3如下图所示:)
也就是说,在执行完这三条指令后edi所指向的地址(0x0037fcd0)和其后面的228个地址里面所存储的内容全被赋值为CCCCCCCC,其实这里0x0037fcd0后面的第228个地址就是ebp所指向的地址。也就是说从edi所指向的地址一直到ebp所指向的地址中间这一部分全被赋值成CCCCCCCC。
值得注意的是:在执行完这三条指令后,edi所指向的地址已经发生改变,此时的edi和edp指向同一地址。
1:变量a的创建
接着就来到了红框圈起来的指令了,mov dword ptr [ebp-8],0Ah的意思是:把0Ah(对应十进制的10)放在edp-8(8是8个字节的意思)这个地址里面。
可见执行完这条指令后,原本ebp-8这个地址所指向空间中的CCCCCCCC被替换成了0Ah(10),说明变量a已经创建完毕。
这里也说明了一个问题,如果只是对变量进行声明而不进行初始化赋值,那内存里面存的就是CCCCCCCC(随机值,只是vs2013放的是CCCCCCCC,其他的编译器可能是其他的值),这也就是为什么我们有时打印变量会出现“烫烫烫烫”的原因
2.变量b的创建 和上面一样,这里的mov dword ptr [ebp-14h],14h指令执行的操作就是把14h(对应十进制的20)存到ebp-14h这个地址所指向的空间里。 注意,这里出现的两个14h没有任何关联,如果让b等于其他任何一个整数,始终都是把这个整数存到ebp-14h所指向的这个空间里面。
可以看出在vs2013这个编译器上,变量a和b在内存中的存储地址相差了8个字节,至于相差几个字节这以取决于编译器,不同的编译器会有不同的效果。
3:变量c的创建 和上面同理
接下来就到了调用Add函数的时候了。

此时出现了两组mov和push。第一组中的mov eax,dword ptr [ebp-14h]意思是,把ebp-14h这个地址里面存放的值(也就是20)赋值给eax,然后push(压栈)eax。同理,第二组先把ebp-8这个地址里面存的值(也就是10)赋值给ecx,然后push(压栈)ecx。
经过这两次压栈后,esp指针指向的地址减小8个字节(一次减小4个字节,也就是1个整型)
在执行call指令之前,先看一下call指令下一条指令的地址,也就是add指令的地址003F1450
执行call指令时我们需要点击F11才能进入到Add函数的内部去一探究竟。
点击F11执行了call指令后,我们不但进入了Add函数的内部,还让esp指针上移了4个字节,为什么会上移呢?那一定是又元素压栈,因为只有这样才能让栈顶指针上移,那我们再来看一下压入的元素是什么呢?不难发现,压入的这个值就是call指令下一条指令add的地址003F1450。
那为什么要把call指令下一条指令的地址存起来呢?是因为在Add函数调用结束的时候,需要返回继续执行call指令的下一条指令,所以在执行call指令的时候要把call指令的下一条指令的地址存下来,在Add函数调用结束的时候,就能根据存的这个地址找到call指令的下一条指令,进而让程序继续进行。
接着再点击一次F11,就真真正正的进入到Add函数的内部了
蓝色的部分是不是特别眼熟,这部分和main函数前面那部分是一样的,就是为Add函数创建函数栈帧。
Add函数中变量z的创建
这里变量z的创建过程和main函数中变量a、b、c的创建过程一模一样,详细过程就不再进行赘述.
Add函数中的求x+y的和 接着往下,终于来到了 z = x + y,要求两数和了
求和分3条指令来完成
通过上面的分析可以看出,在调用Add函数的过程中,并没有创建形参x和y,而是在执行call指令调用Add函数之前就已经进行了传参,当时先让eax等于20(也就是实参b的值)先压栈,再让ecx等于10(也就是实参a的值)进行压栈,实参是按照从右到左的顺序进行传递的。当在Add函数中需要用到形参x的时候,是返回去找到ecx取出ecx里面存的值,这个值就是形参x的值。而当Add函数中需要用到形参y的时候,是返回去找到eax取出eax里面存的值,这个值就是形参y的值。 通过这里我们还可以看出,形参实际上只是实参的一份拷贝,改变形参的值不会影响实参.
接下来,最重要的一步来了:就是把z=30返回到main函数里面,分成以下6条指令来完成:

}的右边表明Add函数调用结束)通过实际的调试也可以看出每执行一次pop指令,esp指针的值就加4这和每执行一次push指令,esp的值就减4形成呼应. 接着执行标号5的指令 mov esp,ebp,这条指令执行的结果就是把ebp存的地址给esp,也就是说,执行完这条指令后,esp和edp指向同一个地址空间。动画演示如下:
接着执行标号6的指令:pop ebp。这条指令的执行结果是:把栈顶数据弹出至edp里面进行储存。此时的栈顶数据是什么呢?通过动画不拿发现:此时的栈顶数据其实就是main函数栈底的地址呀,这样以来,执行完这条指令后edp就又指向main函数的栈底了,而此时栈顶存储的数据也变成了call指令下一条子陵的地址。动画演示如下:
最后执行ret指令**,ret指令会从栈顶弹出元素给IP,也就是下一条要执行的指令的地址**。此时的栈顶存储的就是call指令下一条指令的地址,这就是为什们当时要存call指令下一条指令的地址。是为了在Add函数调用结束后这个黄色的小箭头能够回到正确的地方继续执行接下来的指令
通过上面的调试动图可以看出:在执行完ret指令之后esp的值从个位上的8变成了个位上的c(c对应十进制下的12),二者相差4。说明确实把栈顶数据弹出了。 动画演示如下:
我们已经掌握的内存开辟⽅式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的⽅式有两个特点:
C语⾔引⼊了动态内存开辟,让程序员⾃⼰可以申请和释放空间,就⽐较灵活了。
C语⾔提供了⼀个动态内存开辟的函数:
void* malloc (size_t size);
这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。
C语⾔提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
free函数⽤来释放动态开辟的内存。
举个例⼦:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int arr[num] = {0};
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//是否有必要?
return 0;
}
C语⾔还提供了⼀个函数叫 calloc , calloc 函数也⽤来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
举个例⼦:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if(NULL != p)
{
int i = 0;
for(i=0; i<10; i++)
{
printf("%d ", *(p+i));
}
}
free(p);
p = NULL;
return 0;
}
输出结果
0 0 0 0 0 0 0 0 0 0
所以如果我们对申请的内存空间的内容要求初始化,那么可以很⽅便的使⽤calloc函数来完成任务。
函数原型如下: 1 void* realloc (void* ptr, size_t size); • ptr 是要调整的内存地址 • size 调整之后新⼤⼩ • 返回值为调整之后的内存起始位置。 • 这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到 新 的空间。 • realloc在调整内存空间的是存在两种情况: ◦ 情况1:原有空间之后有⾜够⼤的空间 ◦ 情况2:原有空间之后没有⾜够⼤的空间
如果没有⽂件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运⾏程序,是看不到上次程序的数据的,如果要将数据进⾏持久化的保存,我们可以使⽤⽂件。
磁盘(硬盘)上的⽂件是⽂件。
但是在程序设计中,我们⼀般谈的⽂件有两种:程序⽂件、数据⽂件(从⽂件功能的⻆度来分类的)。
目录
在ANSI C的任何⼀种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执⾏的机器指令(⼆进制指令)。
第2种是执⾏环境,它⽤于实际执行代码。
那翻译环境是怎么将源代码转换为可执⾏的机器指令的呢?这⾥我们就得展开开讲解⼀下翻译环境所做的事情。
其实翻译环境是由编译和链接两个⼤的过程组成的,⽽编译⼜可以分解成:预处理(有些书也叫预编译)、编译、汇编三个过程。
⼀个C语⾔的项⽬中可能有多个.c⽂件⼀起构建,那多个 .c⽂件如何⽣成可执⾏程序呢?
.c⽂件单独经过编译器,编译处理⽣成对应的⽬标⽂件。.obj,Linux环境下⽬标⽂件的后缀是.o如果再把编译器展开成3个过程,那就变成了下⾯的过程:
在预处理阶段,源⽂件和头⽂件会被处理成为.i为后缀的⽂件。 在 gcc 环境下想观察⼀下,对 test.c ⽂件预处理后的.i⽂件,命令如下:
gcc -E test.c -o test.i
预处理阶段主要处理那些源⽂件中#开始的预编译指令。⽐如:#include,#define,处理的规则如下:
.i⽂件中不再包含宏定义,因为宏已经被展开。并且包含的头⽂件都被插⼊到.i⽂件中。所以当我们⽆法知道宏定义或者头⽂件是否包含正确的时候,可以查看预处理后的.i⽂件来确认。编译过程就是将预处理后的⽂件进⾏⼀系列的:词法分析、语法分析、语义分析及优化,⽣成相应的汇编代码⽂件。
编译过程的命令如下:
gcc -S test.i -o
将源代码程序被输⼊扫描器,扫描器的任务就是简单的进⾏词法分析,把代码中的字符分割成⼀系列的记号(关键字、标识符、字⾯量、特殊字符等)。
接下来语法分析器,将对扫描产⽣的记号进⾏语法分析,从⽽产⽣语法树。这些语法树是以表达式为节点的树。
由语义分析器来完成语义分析,即对表达式的语法层⾯分析。编译器所能做的分析是语义的静态分析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息。
汇编器是将汇编代码转转变成机器可执⾏的指令,每⼀个汇编语句⼏乎都对应⼀条机器指令。就是根据汇编指令和机器指令的对照表⼀⼀的进⾏翻译,也不做指令优化。
汇编的命令如下:
gcc -c test.s -o test.o
链接是⼀个复杂的过程,链接的时候需要把⼀堆⽂件链接在⼀起才⽣成可执⾏程序。
链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤。
链接解决的是⼀个项⽬中多⽂件、多模块之间互相调⽤的问题。
比如:
在⼀个C的项⽬中有2个.c⽂件( test.c 和 add.c ),代码如下:
test.c
#include <stdio.h>
//test.c
//声明外部函数
extern int Add(int x, int y);
//声明外部的全局变量
extern int g_val;
int main()
{
int a = 10;
int b = 20;
int sum = Add(a, b);
printf("%d\n", sum);
return 0;
}
add.c
int g_val = 2022;
int Add(int x, int y)
{
return x+y;
}
我们已经知道,每个源⽂件都是单独经过编译器处理⽣成对应的⽬标⽂件。
test.c 经过编译器处理⽣成test.o
add.c 经过编译器处理⽣成add.o
我们在 test.c 的⽂件中使⽤了 add.c ⽂件中的 Add 函数和 g_val 变量。
我们在 test.c ⽂件中每⼀次使⽤ Add 函数和 g_val 的时候必须确切的知道 Add 和 g_val 的地址,但是由于每个⽂件是单独编译的,在编译器编译 test.c 的时候并不知道 Add 函数和 g_val 变量的地址,所以暂时把调⽤ Add 的指令的⽬标地址和 g_val 的地址搁置。等待最后链接的时候由链接器根据引⽤的符号 Add 在其他模块中查找 Add 函数的地址,然后将 test.c 中所有引⽤到 Add 的指令重新修正,让他们的⽬标地址为真正的 Add 函数的地址,对于全局变量 g_val 也是类似的⽅法来修正地址。这个地址修正的过程也被叫做:重定位
前⾯我们⾮常简洁的讲解了⼀个C的程序是如何编译和链接,到最终⽣成可执⾏程序的过程,其实很多内部的细节⽆法展开讲解。⽐如:⽬标⽂件的格式elf,链接底层实现中的空间与地址分配,符号解析和重定位等,如果你有兴趣,可以看《程序的⾃我修养》⼀书来详细了解。
目录
C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的。
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
举个例⼦:
printf("file:%s line:%d\n", __FILE__, __LINE__);
基本语法:
#define name stuff
举个例⼦:
#define MAX 1000
#define reg register //为 register这个关键字,创建⼀个简短的名字
#define do_forever for(;;) //⽤更形象的符号来替换⼀种实现
#define CASE break;case //在写case语句的时候⾃动把 break写上。
// 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
思考:在define定义标识符的时候,要不要在最后加上 ; ? ⽐如:
#define MAX 1000;
#define MAX 1000
建议不要加上 ; ,这样容易导致问题。 ⽐如下⾯的场景:
if(condition)
max = MAX;
else
max = 0;
如果是加了分号的情况,等替换后,if和else之间就是2条语句,⽽没有⼤括号的时候,if后边只能有⼀条语句。这⾥会出现语法错误。
#define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下⾯是宏的申明⽅式:
#define name( parament-list ) stuff
其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。 注意:
参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分。
举例:
#define SQUARE( x ) x * x
这个宏接收⼀个参数 x .如果在上述声明之后,你把 SQUARE( 5 ); 置于程序中,预处理器就会⽤下⾯这个表达式替换上⾯的表达式:5 * 5
警告:
这个宏存在⼀个问题:
观察下⾯的代码段:
int a = 5;
printf("%d\n" ,SQUARE( a + 1) );
乍⼀看,你可能觉得这段代码将打印36,事实上它将打印11,为什么呢?
替换⽂本时,参数x被替换成a + 1,所以这条语句实际上变成了:
printf ("%d\n",a + 1 * a + 1 );
这样就⽐较清晰了,由替换产⽣的表达式并没有按照预想的次序进⾏求值。在宏定义上加上两个括号,这个问题便轻松的解决了:
#define SQUARE(x) (x) * (x)
这样预处理之后就产⽣了预期的效果:
printf ("%d\n",(a + 1) * (a + 1) );
这⾥还有⼀个宏定义:
#define DOUBLE(x) (x) + (x)
定义中我们使⽤了括号,想避免之前的问题,但是这个宏可能会出现新的错误。
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));
这将打印什么值呢?看上去,好像打印100,但事实上打印的是55. 我们发现替换之后:
printf ("%d\n",10 * (5) + (5));
乘法运算先于宏定义的加法,所以出现了 55 . 这个问题,的解决办法是在宏定义表达式两边加上⼀对括号就可以了。
#define DOUBLE( x) ( ( x ) + ( x ) )
提⽰:
所以⽤于对数值表达式进⾏求值的宏定义都应该⽤这种⽅式加上括号,避免在使⽤宏时由于参数中的操作符或邻近操作符之间不可预料的相互作⽤。
当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。 例如:
x+1;//不带副作⽤
x++;//带有副作⽤
MAX宏可以证明具有副作⽤的参数所引起的问题。
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
这⾥我们得知道预处理器处理之后的结果是什么:
z = ( (x++) > (y++) ? (x++) : (y++));
所以输出的结果是:x=6 y=10 z=9
在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤。
注意:
宏通常被应⽤于执⾏简单的运算。
⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不⽤函数来完成这个任务?
原因有⼆:
和函数相⽐宏的劣势:
宏有时候可以做函数做不到的事情。⽐如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type)
(type )malloc(num sizeof(type))
...
//使⽤
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 sizeof(int));
#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。#运算符所执⾏的操作可以理解为”字符串化“。
当我们有⼀个变量int a = 10; 的时候,我们想打印出:the value of a is 10.
就可以写:
#define PRINT(n) printf("the value of "#n " is %d", n);
当我们按照下⾯的⽅式调⽤的时候:
PRINT(a);//当我们把a替换到宏的体内时,就出现了#a,⽽#a就是转换为"a",时⼀个字符串代码就会被预处理为:
printf("the value of ""a" " is %d", a);
运⾏代码就能在屏幕上打印:
the value of a is 10
## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符.
## 被称为记号粘合.
这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。
这⾥我们想想,写⼀个函数求2个数的较⼤值的时候,不同的数据类型就得写不同的函数。
⽐如:
int int_max(int x, int y)
{
return x>y?x:y;
}
float float_max(float x, float y)
{
return x>y?x:y;
}
但是这样写起来太繁琐了,现在我们这样写代码试试:
//宏定义
#define GENERIC_MAX(type)
type type##_max(type x, type y)
{
return (x>y?x:y);
}
使⽤宏,定义不同函数:
GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名
int main()
{
//调⽤函数
int m = int_max(2, 3);
printf("%d\n", m);
float fm = float_max(3.5f, 4.5f);
printf("%f\n", fm);
return 0;
}
输出:
3
4.500000
在实际开发过程中##使⽤的很少,很难取出⾮常贴切的例⼦。
⼀般来讲函数的宏的使⽤语法很相似。所以语⾔本⾝没法帮我们区分⼆者。
那我们平时的⼀个习惯是:
把宏名全部⼤写
函数名不要全部⼤写
这条指令⽤于移除⼀个宏定义。
#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。
许多C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。
例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个
机器内存⼤些,我们需要⼀个数组能够⼤些。)
#include <stdio.h>
int main()
{
int array [ARRAY_SIZE];
int i = 0;
for(i = 0; i< ARRAY_SIZE; i ++)
{
array[i] = i;
}
for(i = 0; i< ARRAY_SIZE; i ++)
{
printf("%d " ,array[i]);
}
printf("\n" );
return 0;
}
编译指令:
//linux 环境演⽰
gcc -D ARRAY_SIZE=10 programe.c
在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令。
⽐如说:
调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
常见的条件编译指令:
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多个分⽀的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
#include "filename"
查找策略:先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在标准位置查找头⽂件。 如果找不到就提⽰编译错误。
/usr/include
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径
#include <filename.h>
查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。 这样是不是可以说,对于库⽂件也可以使⽤ “” 的形式包含? 答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件了。
我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的地⽅⼀样。
这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。
⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤。
test.c
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}
test.h
void test();
struct Stu
{
int id;
char name[20];
};
如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。
如果test.h ⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。
如何解决头⽂件被重复引⼊的问题?答案:条件编译。
每个头⽂件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__
或者
#pragma once
就可以避免头⽂件的重复引⼊。 注: 推荐《⾼质量C/C++编程指南》中附录的考试试卷(很重要)。 笔试题:
#error
#pragma
#line
...
不做介绍,⾃⼰去了解。
#pragma pack()在结构体部分介绍。
为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员开发软件。
简单的总结,C语言常用的库函数都有:
目录:
注意:
为了让光标移到下⼀⾏的开头,可以在输出⽂本的结尾,添加⼀个换⾏符 \n 。
#include <stdio.h>
int main(void)
{
printf("Hello World\n");
return 0;
}
如果⽂本内部有换⾏,也是通过插⼊换⾏符来实现,如下⽅代码:
#include <stdio.h>
int main(void)
{
printf("Hello\nWorld\n");
printf("Hello\n");
printf("World\n");
return 0;
}
printf() 的占位符有许多种类,与 C 语⾔的数据类型相对应。下⾯按照字⺟顺序,列出常⽤的占位符,⽅便查找,具体含义在后⾯章节介绍。
这⾥的 %d , %c 等是占位符,会被后边的值替换。
printf("year=%d\n",year);
printf("year=%02d\n",year);
printf("%c\n",'w');
printf("%c\n",97);
printf("abcdef\n");//只有字符串可以这样
printf("%s\n","abcdef");
printf("%s will come tonight\n", "zhangsan");
一个引号引起来的是字符,两个引号引起来的是字符串。
.2表示保留两位小数,同样最后一位会根据它后面的那一位四舍五入;如果本来给的浮点数没有达到要保留的位数,则会在后面添0.printf("%f\n",58.8888888);
printf("%.2f",58.88888);
结果:
58.888889
58.89
printf("%p\n",&a);
直接转义某个字符
printf() 参数与占位符是⼀⼀对应关系,如果有 n 个占位符,printf() 的参数就应该有 n + 1 个。如果参数个数少于对应的占位符, printf() 可能会输出内存中的任意值。
%d,%%d,%%%d和\\%d的区别%d,表示按整型输出后面给出的变量的值。
%%d ,这就会被拆成两部分看待,一是“%%”在C语言中就是输出一个“%”,而是“d”就是一个普通字符,所以当“%%d”在一起时,其含义就是输出“%d”这两个字符。
%%%d ,3个%在一起,进行拆分的话,%%代表一个“%”字符,后面的%d又代表整型输出变量的值,所以当“%%%d”一起时,其最终含义就是输出一个字符%号再接着按整型输出变量的值。类似的\符号也是一样。
C语言中,%是转义符,也就是和%一起出现的后面的内容会转义后输出
可以定制占位符的输出格式。
#include <stdio.h>
int main()
{
printf("%5d\n", 123); // 输出为 " 123"
return 0;
}
上⾯⽰例中, %5d 表⽰这个占位符的宽度⾄少为5位。如果不满5位,对应的值的前⾯会添加空格。输出的值默认是右对⻬,即输出内容前⾯会有空格;如果希望改成左对⻬,在输出内容后⾯添加空格,可以在占位符的 % 的后⾯插⼊⼀个-号。
#include <stdio.h>
int main()
{
printf("%-5d\n", 123); // 输出为 "123 "
return 0;
}
对于⼩数,这个限定符会限制所有数字的最⼩显⽰宽度。
// 输出 " 123.450000",前面有两个空格
#include <stdio.h>
int main()
{
printf("%12f\n", 123.45);
return 0;
}
上⾯⽰例中, %12f 表⽰输出的浮点数最少要占据12位。由于⼩数的默认显⽰精度是⼩数点后6位,所以 123.45 输出结果的头部会添加2个空格。
+-号
默认情况下, printf() 不对正数显⽰ + 号,只对负数显⽰ - 号。如果想让正数也输出 + 号,可以在占位符的 % 后⾯加⼀个 + 。#include <stdio.h>
int main()
{
printf("%+d\n", 12); // 输出 +12
printf("%+d\n", -12); // 输出 -12
return 0;
}
// 输出 Number is 0.50
#include <stdio.h>
int main()
{
printf("Number is %.2f\n", 0.5);
return 0;
}
上⾯⽰例中,如果希望⼩数点后⾯输出3位( 0.500 ),占位符就要写成 %.3f 。
这种写法可以与限定宽度占位符,结合使⽤。
// 输出为 " 0.50",前面有俩空格
#include <stdio.h>
int main()
{
printf("%6.2f\n", 0.5);
return 0;
}
上⾯⽰例中, %6.2f 表⽰输出字符串最⼩宽度为6,⼩数位数为2。所以,输出字符串的头部有两个空格。
#include <stdio.h>
int main()
{
printf("%*.*f\n", 6, 2, 0.5);
return 0;
}
// 等同于printf("%6.2f\n", 0.5);
上⾯⽰例中, %*.*f 的两个星号通过 printf() 的两个参数 6 和 2 传⼊。
// 输出 hello
#include <stdio.h>
int main()
{
printf("%.5s\n", "hello world");
return 0;
}
上⾯⽰例中,占位符 %.5s 表⽰只输出字符串“hello world”的前5个字符,即“hello”。
printf函数是有返回值的,返回的是输出的字符串的长度
int main()
{
int a = printf("hehe");
//不能在这个之后直接加上\n,原因是返回值会把\n也算进去,返回值就多了1
printf("\n%d\n",a);//注意\n也可以写在前面
return 0;
}
scanf的功能:用一句话来概括就是“通过键盘给程序中的变量赋值”。 程序运⾏到这个语句时,会停下来,等待⽤⼾从键盘输⼊。
⽤⼾输⼊数据、按下回⻋键后, scanf() 就会处理⽤⼾的输⼊,将其存⼊变量。它的原型定义在头⽂件stdio.h。
函数的原型
int scanf(const char *format, ...);
作用:将从键盘输入的字符转化为输入控制符所规定格式的数据,然后存入已输入参数的值为地址的变量中。
scanf("%d", &i)
注意:变量前⾯必须加上 & 运算符(指针变量除外),因为 scanf() 传递的不是值,⽽是地址,即将变量 i 的地址指向⽤⼾输⼊的值。 如果这⾥的变量是指针变量(⽐如字符串变量),那就不⽤加 & 运算符。
scanf() 处理数值占位符时,会⾃动过滤空⽩字符,包括空格、制表符、换⾏符等。
所以,⽤⼾输⼊的数据之间,有⼀个或多个空格不影响 scanf() 解读数据。另外,⽤⼾使⽤回⻋键,将输⼊分成⼏⾏,也不影响解读。
1
-20
3.4
-4.0e3
上⾯⽰例中,⽤⼾分成四⾏输⼊,得到的结果与⼀⾏输⼊是完全⼀样的。每次按下回⻋键以后,scanf() 就会开始解读,如果第⼀⾏匹配第⼀个占位符,那么下次按下回⻋键时,就会从第⼆个占位符开始解读。
scanf() 处理⽤⼾输⼊的原理是,⽤⼾的输⼊先放⼊缓存,等到按下回⻋键后,按照占位符对缓存进⾏解读。解读⽤⼾输⼊时,会从上⼀次解读遗留的第⼀个字符开始,直到读完缓存,或者遇到第⼀个不符合条件的字符为⽌。
#include <stdio.h>
int main()
{
int x;
float y;
// ⽤⼾输⼊ " -13.45e12# 0"
scanf("%d", &x);
printf("%d\n", x);
scanf("%f", &y);
printf("%f\n", y);
return 0;
}
上⾯⽰例中, scanf() 读取⽤⼾输⼊时, %d 占位符会忽略起⾸的空格,从 - 处开始获取数据,读取到 -13 停下来,因为后⾯的 . 不属于整数的有效字符。这就是说,占位符 %d 会读到 -13 。
第⼆次调⽤ scanf() 时,就会从上⼀次停⽌解读的地⽅,继续往下读取。这⼀次读取的⾸字符是.,由于对应的占位符是 %f ,会读取到 .45e12 ,这是采⽤科学计数法的浮点数格式。后⾯的#不属于浮点数的有效字符,所以会停在这⾥。
由于 scanf() 可以连续处理多个占位符,所以上⾯的例⼦也可以写成下⾯这样。
#include <stdio.h>
int main()
{
int x;
float y;
// ⽤⼾输⼊ " -13.45e12# 0"
scanf("%d%f", &x, &y);
return 0;
}
scanf() 的返回值是⼀个整数,表⽰成功读取的变量个数。
如果没有读取任何项,或者匹配失败,则返回 0 。如果在成功读取任何数据之前,发⽣了读取错误或者遇到读取到⽂件结尾,则返回常量EOF。
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
float f = 0.0f;
int r = scanf("%d %d %f", &a, &b, &f);
printf("a=%d b=%d f=%f\n", a, b, f);
printf("r = %d\n", r);
return 0;
}
如果⼀个数字都不输⼊,直接按3次 ctrl+z退出 ,输出的r是-1,也就是EOF。
scanf()常⽤的占位符如下,与 printf()的占位符基本⼀致。
%c:字符。%d:整数。%f:float类型浮点数。%lf:double类型浮点数。%Lf:long double类型浮点数。(大写L)%s:字符串。%[]:在⽅括号中指定⼀组匹配的字符(⽐如 %[0-9]),遇到不在集合之中的字符,匹配将会停⽌。上⾯所有占位符之中,除了%c以外,都会⾃动忽略起⾸的空⽩字符。
%c 不忽略空⽩字符,总是返回当前第⼀个字符,⽆论该字符是否为空格。如果要强制跳过字符前的空⽩字符,可以写成
scanf(" %c", &ch) ,即 %c 前加上⼀个空格,表⽰跳过零个或多个空⽩字符。
下⾯要特别说⼀下占位符%s,它其实不能简单地等同于字符串。它的规则是,从当前第⼀个⾮空⽩字符开始读起,直到遇到空⽩字符(即空格、换⾏符、制表符等)为⽌。
因为 %s 不会包含空⽩字符,所以⽆法⽤来读取多个单词,除⾮多个 %s ⼀起使⽤。这也意味着,scanf() 不适合读取可能包含空格的字符串,⽐如书名或歌曲名。另外, scanf() 遇到 %s 占位符,会在字符串变量末尾存储⼀个空字符 \0。
scanf() 将字符串读⼊字符数组时,不会检测字符串是否超过了数组⻓度。所以,储存字符串时,很可能会超过数组的边界,导致预想不到的结果。为了防⽌这种情况,使⽤ %s 占位符时,应该指定 读⼊字符串的最⻓⻓度,即写成 %[m]s ,其中的 [m] 是⼀个整数,表⽰读取字符串的最⼤⻓度,后⾯的字符将被丢弃。
#include <stdio.h>
int main()
{
char name[11];
scanf("%10s", name);
return 0;
}
上⾯⽰例中, name 是⼀个⻓度为11的字符数组, scanf() 的占位符 %10s 表⽰最多读取⽤⼾输⼊的10个字符,后⾯的字符将被丢弃,这样就不会有数组溢出的⻛险了。
以下代码的目的是从一串包含生日信息的数字中输出,输出年,月,日的信息.
#include <stdio.h>
int main()
{
int year = 0;
int month = 0;
int day =0;
scanf("%4d%2d%2d", &year, &month, &day);
//这里的4d,2d,2d是指指定的扫描宽度,从前往后,超过的的部分将不会被扫描
//这里的%4d%2d%2d与输入的与那一串数字格式是一样的
printf("year=%d\n",year);
printf("month=%02d\n",month);
//02d的意思是,指定输出的宽度为2,宽度不足2的前面拿0来补足,d指的是整型,另外,如果只是2d,没有0,如果宽度不够,那么他会输出 空格+数字
printf("day=%02d\n",day);
}
以下代码的目的是指定格式的信息中提取再输出
其中学号和成绩用;隔开,成绩和成绩之间使用,隔开.
int main()
{
int id = 0;
float c = 0.0f;//float类型创建的变量默认是double,在后面加上f则会强制锁定float类型
float math = 0.0f;
float english = 0.0f;
scanf("%d;%f,%f,%f", &id, &c, &math, &english);
//这里的%d;%f,%f,%f与输入的与那一串数字格式是一样的
printf("The each subject score of NO. %d is %.2f, %.2f, %.2f", id, c, math, english);
}
例子:
#include<stdio.h>
int main()
{
int a,b;
printf("请输入整数:");
scanf("%d",&a);// %d,将输入的字符转化为十进制形式
printf("a=%d\n",a);// %d,以十进制输出a的值,\n换行符
b=a>0?1:-1;//三目运算符,当a>0,b=1;否则b=-1
printf("b=%d\n",b);
return 0;
}
作用:将从键盘输入的字符转化为输入控制符所规定格式的数据,然后存入已输入参数的值为地址的变量中(非输入控制符必须原样输入)。
例子:
#include<stdio.h>
int main()
{
int a,b;
printf("请输入整数:");
scanf("a=%d",&a);// %d,将输入的字符转化为十进制形式
//这里不一样
b=a>0?1:-1;//三目运算符,当a>0,b=1;否则b=-1
printf("b=%d\n",b);
return 0;
}
根据scanf("a=%d",&a)双引号里的内容”a=%d",在终端输入必须输入“a=数字",然后回车,注意"a="不能少,否则程序不能向下执行。
极端一点的例子:
scanf("%d, %daaa", &a, &b);
输入时必须输入"6,8aaa"格式才可以,空格倒无所谓
总结:推荐第一种用法,尽量不要用非输入控制符,可以结合printf使用来提示输入的东西,然后根据提示直接输入就好。
有时,⽤⼾的输⼊可能不符
#include <stdio.h>
int main()
{
int year = 0;
int month = 0;
int day = 0;
scanf("%d-%d-%d", &year, &month, &day);
printf("%d %d %d\n", year, month, day);
return 0;
}
上⾯⽰例中,如果⽤⼾输⼊ 2020-01-01 ,就会正确解读出年、⽉、⽇。问题是⽤⼾可能输⼊其他格式,⽐如 2020/01/01 ,这种情况下, scanf() 解析数据就会失败。为了避免这种情况, scanf() 提供了⼀个赋值忽略符(assignment suppression character) * 。
只要把 * 加在任何占位符的百分号后⾯,该占位符就不会返回值,解析后将被丢弃。
#include <stdio.h>
int main()
{
int year = 0;
int month = 0;
int day = 0;
scanf("%d%*c%d%*c%d", &year, &month, &day);
return 0;
}
上⾯⽰例中,%*c 就是在占位符的百分号后⾯,加⼊了赋值忽略符 * ,表⽰这个占位符没有对应的变量,解读后不必返回。
应该在输入前有提示.
printf(请输入……"");
scanf("%d", &a);
如何用scanf编写高质量代码?
char ch;
while('\n' != (ch = getchar()))
{
continue;
}
例子2:
#include<stdio.h>
int main(void)
{
int a, c;
char ch;
scanf("%d\n", &a);
printf("%d\n", a);
while('\n' != (ch = getchar()))
{
continue;
}
// 加上面代码可解决此问题,功能是把输入的字符全部接收,
// 也即输入的字符应经被清空,可进行下次输入。
scanf("%d", &c);
printf("%d\n", c);
}
若直接输入99m,则a = 99,c = 垃圾值.这是因为a把99m中的99当做有效值接收,而c从m开始接收,出错.scanf中没有被接收的值不会自动清除,而是保留等下个变量再来接收.
注意:scanf函数在遇到空格或换行符时会停止读取.
int main()
{
i = 0;
arr[4] = {0};
while(i<4)
{
scanf("%d",&arr[i]);
i++;
}
return 0;
}
getchar()和putchar()是一对字符输入/输出函数。getchar()不带任何参数,getchar()用于读取用户从键盘输入的单个字符。putchar()向终端输出一个字符,其格式为putchar()。getchar()和putchar()函数包含在头文件stdio.h中,使用时需包含此头文件。
举例:
#include<stdio.h>
int main()
{
int ch = getchar();//实际变量ch中放的是读到的那个字符的ASCII码值,键盘输入
putchar(ch);//putchar接收到一个参数(ASCII码值),输出相对应的字符
return 0;
}
getchar()函数的返回值类型时整形,当发生读取错误时,返回整型值是-1,把一个负值赋给一个char型的变量是不正确的。当读取正确时,他会返回用户从键盘输的第一个字符的ASCII码值,ASCII码值是数字符号,通过这里也可以看出来getchar()返回值类型应用int定义.
它们的工作原理有相同也有不同
相同之处:
getchar()和scanf()不是直接从键盘上拿数据,他们是从键盘的缓冲区拿数据,键盘输入的字符会放入缓冲区,若用户不按回车键,所有放入缓冲区的字符都不会被读。
不同之处:
在用户按下回车键后,缓冲区内会存在’\n’,scanf只会都'\n'之前的字符,不读' \n'和空格. getchar会将缓冲区的所有字符全部读走,其中包括空格和'\n'。在windows下如果想结束,就输入Ctrl+Z,表示EOF.
以下的代码适当的修改是可以用来清理缓冲区的.
//代码1
#include <stdio.h>
int main()
{
int ch = 0;
while ((ch = getchar()) != EOF)//说明得到的是正常的字符
putchar(ch);
return 0;//ctrl+Z可以让代码停下来
}
以下的代码用来演示使用getchar()和putchar()实现更加复杂的功能
#include<stdio.h>
int main()
{
char password[20] = { 0 };
printf("请输入密码:>");
scanf("%s", password);
//注意这里没有取地址`&`,原因是数组的地址已经是在创建的过程中就已经建好了的
//scanf不是直接从键盘拿数据,scanf的工作原理是:在scanf和键盘之间的输入缓冲区中拿数据,输入缓冲区有数据他就拿,没有他就等,当从键盘上输入字符abcdef为了让字符abcdef来到缓冲区
//在键盘上输入\n(回车)字符连同\n一起来到缓冲区,scanf会拿走\n之前的字符abcdef缓冲区剩下\n
//scanf不读空格和回车
int tmp = 0;
while ((tmp = getchar()) != '\n')//用来清理缓冲区//注意这种写法((tmp = getchar())
{
;//啥也没干,或着加上(continue)
}
printf("请确认密码(y/n):");
int ch = getchar(); //getchar和scanf的工作原理一样,他会读走缓冲区里剩余的\n,ch里边是\n
if (ch == 'y') //getchar和putchar每次只会输入和输出一个字符
{
printf("确认成功\n");
}
else
{
printf("确认失败\n");
}
return 0;
}
以下代码的作用是:只打印数字字符,跳过其他字符的
//代码2
#include <stdio.h>
int main()
{
char ch = '\0';
while ((ch = getchar()) != EOF)
{
if (ch < '0'|| ch > '9')
continue;
putchar(ch);
}
return 0;
}
putchar和getchar是输入输出单字符的,printf和scanf可以输入多字符,并且getchar和putchar只可以用于字符型的输入输出,而scanf和printf可以用于整型,浮点型和字符型等类型的输入和输出。
注意: putchar()输出指定的字符,不会在输出后自动换行,所以putchar(a)和putchar(b)之间要加putchar('\n'),用作换行.
size_t strlen ( const char * str );
#include <stdio.h>
#include <string.h>
int main()
{
const char* str1 = "abcdef";
const char* str2 = "bbb";
if(strlen(str2)-strlen(str1)>0)
{
printf("str2>str1\n");
}
else
{
printf("srt1>str2\n");
}
return 0;
}
strlen的模拟实现:
方式1:
//计数器⽅式
int my_strlen(const char * str)
{
int count = 0;
assert(str);
while(*str)
{
count++;
str++;
}
return count;
}
⽅式2:
//不能创建临时变量计数器
int my_strlen(const char * str)
{
assert(str);
if(*str == '\0')
return 0;
else
return 1+my_strlen(str+1);
}
方式3:
//指针-指针的⽅式
int my_strlen(char *s)
{
assert(str);
char *p = s;
while(*p != ‘\0’ )
p++;
return p-s;
}
使用
char* strcpy(char * destination, const char * source );
Copies the C string pointed by source into the array pointed by destination, including theterminating null character (and stopping at that point).
注意:
模拟实现
char* my_strcpy(char *dest,const char*src)
//这里返回类型是char*,目的是为了实现链式访问
//返回值应该是目标位置的起始地址
{
char *ret = dest;
assert(dest != NULL);
assert(src != NULL);
while((*dest++ = *src++))
{
;
}
return ret;
}
使用
char *strcat( char *strDestination, const char *strSource );
Appends a copy of the source string to the destination string. The terminating null character in destination is overwritten by the first character of source, and a null-character is included at the end of the new string formed by the concatenation of both in destination.
模拟实现
#include <stdio.h>
#include <assert.h>
char* my_strcat(char* dest, const char* src)
{
char* ret = dest;
assert(dest && src);
while (*dest )
{
dest++;
}
while ((*dest++ = *src++))
{
;
}
return ret;
}
int main()
{
char str1[10] = "abc";
char str2[] = "def";
my_strcat(str1,str2);
printf("%s\n",str1);
return 0;
}
int strcmp( const char *string1, const char *string2 );
使用 This function starts comparing the first character of each string. If they are equal to each other, it continues with the following pairs until the characters differ or until a terminating null-character is reached.
标准规定:
模拟实现
int my_strcmp (const char * str1, const char * str2)
{
int ret = 0 ;
assert(str1 != NULL);
assert(str2 != NULL);
while(*str1 == *str2)
{
if(*str1 == '\0')
return 0;
str1++;
str2++;
}
return *str1-*str2;
}
char *strncpy( char *strDest, const char *strSource, size_t count );
Copies the first num characters of source to destination. If the end of the source C string (which is signaled by a null-character) is found before num characters have been copied, destination is padded with zeros until a total of num characters have been written to it.
char *strncat( char *strDest, const char *strSource, size_t count );
Appends the first num characters of source to destination, plus a terminating null-character. (将source指向字符串的前num个字符追加到destination指向的字符串末尾,再追加一个\0 字符)。
If the length of the C string in source is less than num, only the content up to the terminating null-character is copied.(如果source 指向的字符串的长度小于num的时候,只会将字符串中到\0 的内容追加到destination指向的字符串末尾)。
/* strncat example */
#include <stdio.h>
#include <string.h>
int main ()
{
char str1[20];
char str2[20];
strcpy (str1,"To be ");
strcpy (str2,"or not to be");
strncat (str1, str2, 6);
printf("%s\n", str1);
return 0;
}
int strncmp ( const char * str1, const char * 1 str2, size_t num );
比较str1和str2的前num个字符,如果相等就继续往后比较,最多比较num个字母,如果提前发现不一样,就提前结束,大的字符所在的字符串大于另外一个。如果num个字符都相等,就是相等返回0.
char *strstr( const char *string, const char *strCharSet );
/* strstr example */
#include <stdio.h>
#include <string.h>
int main ()
{
char str[] ="This is a simple string";
char * pch;
pch = strstr (str,"simple");
strncpy (pch,"sample",6);
printf("%s\n", str);
return 0;
}
strstr的模拟实现:
char * strstr (const char * str1, const char * str2)
{
char *cp = (char *) str1;
char *s1, *s2;
if ( !*str2 )
return((char *)str1);
while (*cp)
{
s1 = cp;
s2 = (char *) str2;
while ( *s1 && *s2 && !(*s1-*s2) )
s1++, s2++;
if (!*s2)
return(cp);
cp++;
}
return(NULL);
}
用于切割字符串
char *strtok( char *strToken, const char *strDelimit );
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "192.168.6.111";
char* sep = ".";
char* str = NULL;
for (str = strtok(arr, sep); str != NULL; str = strtok(NULL, sep))
{
printf("%s\n", str);
}
return 0;
}
Get a system error message (strerror) or prints a user-supplied error message (_strerror).
char *strerror( int errnum );
strerror 函数可以把参数部分错误码对应的错误信息的字符串地址返回来。在不同的系统和C语言标准库的实现中都规定了一些错误码,一般是放在errno.h 这个头文件中说明的,C语言程序启动的时候就会使用一个全局的变量errno来记录程序的当前错误码,只不过程序启动 的时候errno是0,表示没有错误,当我们在使用标准库中的函数的时候发生了某种错误,就会将对应的错误码,存放在errno中,而一个错误码的数字是整数很难理解是什么意思,所以每一个错误码都是有对应的错误信息的。strerror函数就可以将错误对应的错误信息字符串的地址返回。
#include <errno.h>
#include <string.h>
#include <stdio.h>
//我们打印一下0~10这些错误码对应的信息
int main()
{
int i = 0;
for (i = 0; i <= 10; i++)
{
printf("%s\n", strerror(i));
}
return 0;
}
结果如下:
No error
Operation not permitted
No such file or directory
No such process
Interrupted function call
Input/output error
No such device or address
Arg list too long
Exec format error
Bad file descriptor
No child processes
举例:
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main ()
{
FILE * pFile;
pFile = fopen ("unexist.ent","r");
if (pFile == NULL)
printf ("Error opening file unexist.ent: %s\n", strerror(errno));
return 0;
}
输出:
Error opening file unexist.ent: No such 1 file or directory
也可以了解一下 perror 函数,perror函数相当于一次将上述代码中的第9行完成了,直接将错误信息打印出来。perror函数打印完参数部分的字符串后,再打印一个冒号和一个空格,再打印错误信息。
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main ()
{
FILE * pFile;
pFile = fopen ("unexist.ent","r");
if (pFile == NULL)
perror("Error opening file unexist.ent");
return 0;
}
输出:
Error opening file unexist.ent: No such 1 file or directory
C语⾔中有⼀系列的函数是专⻔做字符分类的,也就是⼀个字符是属于什么类型的字符的。
这些函数的使⽤都需要包含⼀个头⽂件是ctype.h
这些函数的使⽤⽅法⾮常类似,我们就讲解⼀个函数的事情,其他的⾮常类似:
int islower ( int c );
islower 是能够判断参数部分的 c 是否是⼩写字⺟的。
通过返回值来说明是否是⼩写字⺟,如果是⼩写字⺟就返回⾮0的整数,如果不是⼩写字⺟,则返回0。
写⼀个代码,将字符串中的⼩写字⺟转⼤写,其他字符不变。
#include <stdio.h>
#include <ctype.h>
int main ()
{
int i = 0;
char str[] = "Test String.\n";
char c;
while (str[i])
{
c = str[i];
if (islower(c))
c -= 32;
putchar(c);
i++;
}
return 0;
}
C语⾔提供了2个字符转换函数:
int tolower ( int c ); //将参数传进去的⼤写字⺟转⼩写
int toupper ( int c ); //将参数传进去的⼩写字⺟转⼤写
上⾯的代码,我们将⼩写转⼤写,是-32完成的效果,有了转换函数,就可以直接使⽤ tolower 函数。
#include <stdio.h>
#include <ctype.h>
int main ()
{
int i = 0;
char str[] = "Test String.\n";
char c;
while (str[i])
{
c = str[i];
if (islower(c))
c = toupper(c);
putchar(c);
i++;
}
return 0;
}
void * memcpy ( void * destination, const void * source, 1 size_t num );
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[10] = { 0 };
memcpy(arr2, arr1, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
对于重叠的内存,交给memmove来处理。
memcpy函数的模拟实现:
void * memcpy ( void * dst, const void * src, size_t count)
{
void * ret = dst;
assert(dst);
assert(src);
/*
* copy from lower addresses to higher addresses
*/
while (count--)
{
*(char *)dst = *(char *)src;
dst = (char *)dst + 1;
src = (char *)src + 1;
}
return(ret);
}
void * memmove ( void * destination, const void * source, size_t num );
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
memmove(arr1+2, arr1, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
输出结果:
1 2 1 2 3 4 5 8 9 10
memmove的模拟实现:
void * memmove ( void * dst, const void * src, size_t count)
{
void * ret = dst;
if (dst <= src || (char *)dst >= ((char *)src + count)) {
/*
* Non-Overlapping Buffers
* copy from lower addresses to higher addresses
*/
while (count--)
{
*(char *)dst = *(char *)src;
dst = (char *)dst + 1;
src = (char *)src + 1;
}
}
else {
/*
* Overlapping Buffers
* copy from higher addresses to lower addresses
*/
dst = (char *)dst + count - 1;
src = (char *)src + count - 1;
while (count--) {
*(char *)dst = *(char *)src;
dst = (char *)dst - 1;
src = (char *)src - 1;
//上面这段代码可以简化为以下代码:
//while(count--){
// *((char*)dst+count)=*((char*)src+count);
//}
}
}
return(ret);
}
void * memset ( void * ptr, int value,size_t num );
memset是用来设置内存的,将内存中的值以字节为单位设置成想要的内容。
#include <stdio.h>
#include <string.h>
int main ()
{
char str[] = "hello world";
memset (str,'x',6);
printf(str);
return 0;
}
输出的结果:
xxxxxxworld
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
#include <stdio.h>
#include <string.h>
int main()
{
char buffer1[] = "DWgaOtP12df0";
char buffer2[] = "DWGAOTP12DF0";
int n;
n = memcmp(buffer1, buffer2, sizeof(buffer1));
if (n > 0)
printf("'%s' is greater than '%s'.\n", buffer1, buffer2);
else if (n < 0)
printf("'%s' is less than '%s'.\n", buffer1, buffer2);
else
printf("'%s' is the same as '%s'.\n", buffer1, buffer2);
return 0;
}
使用^和_即可
a2+b3=1
注意:各种运算符和命令产生的格式效果都只对其后面大括号内的各字符有效 (若其中只有一个字符则可省略大括号)
ax+y+z+bij=c
注意:英文字母只有在表示变量时(或单一字符的函数名称,如F(x))时才可以使用斜体,其余情况都应使用罗马体(直立体).
在大括号内最前方加上\rm+空格+要输入的字母
或者,在大括号内最前方加上\text+空格+要输入的字母
\rm(roman,罗马体)
\text(文本)
问两者有何不同?
\text{A B},\rm{A B}
\text A B,\rm A B
如下所示
例如:在表示不同元素时,xi,xj,其中的i,j表示1,2,3,...,n,为变量
而当字母表示特殊含义时,如xi此时这个i表示input的含义,为文本
直立体的例子还有
\text{i},\text{j}
如下方所示:上面的是公式模式下,默认字母的输出,下面的是直立体输出
使用\frac{}{},前一个大括号里面是分子,后一个大括号里面是分母
如果是单个字符,可以直接用空格去替代,如\frac 1 2
可以嵌套frac,如:\frac{\frac{1}{x+y}+1}{y+1}
\frac{}{}
\frac 1 2
\frac{\frac{1}{x+y}+1}{y+1}
如下所示
分子或者分母中的嵌套的分式显示较小,可以通过\dfrac(dispaly-style)
演示
使用\sqrt(square root,平方根)
平方根\sqrt 2,\sqrt{x+y}\\
n次方根\sqrt[n]{a+b}
如下所示
+-><(常见)
\times乘,\cdot点乘,
\div(**div**ide)除
\pm(plus-minus)正负号,\mp(minus-plus)负正号
\ge(greater than or equal,大于等于)
\le(less than or equal,小于等于)
\gg远大于,\ll远小于
\ne(not equal不等于)
\approx(**approx**iamte,约等于)
\equiv(equivalent,恒等的)
\cap交集,\cup并集
\in属于,\notin不属于
\subseteq是...的子集,\subsetneqq是...的真子集
\varnothing空集
\forall任意,\exists存在,\nexists不存在
\because,\therefore\
演示:
\mathbb(blackboard bold)+空格+字母
或者,直接+字母
\mathbb R,\R,\Q,\N,\z_+
如下所示
点:
\cdots横向三个点
\vdots竖向三个点
\ddots斜线三个点(diagonal,对角的)
演示:
一些符号:
\infty(infinity,无穷)
\partial(偏微分的微元符号)
\nabla
\propto(proportional to,正比于)
\degree(度)
演示:
三角函数:
\sin x,\sec x,\cosh x
对数:
\log_2 x,\ln x,\lg x
极限:
\lim_{x \to 0} \frac {x}{\sinx}
注:这些符号都应该是直立体
演示
\sum(求和),\prod(product,乘积)
\sum_i,\sum_{i=0}^N
\frac{\sum_{i=1}^n x_i}{\prod_{i=1}^n x_i}
演示:
可通过在\sum和\prod后面加上\limits来强制n和i=0到求和符号上下
\frac{\sum_{i=1}^n x_i}{\prod_{i=1}^n x_i}
\frac{\sum\limits_{i=1}^n x_I}{\prod\limits_{i=1}^n x_i}
如下
\int(integral积分)
\iint(双重积分)
\iiint(三重积分)
\oint(回路积分),\oiint(双重回路积分)
\int_{-\infty}^0 f(x)\,\text d x
演示:
a\,a
a\ a
a\quad a
a\qquad a
\vec x,\overrightarrow {AB}(向量)
\bar x(平均值),\overline{AB}
演示:
\leftarrow(单箭头)
\rightarrow
*希腊字母英文对照表*
反斜杠加上标准的希腊字母英文拼写
大写的希腊字母就把开头第一个字母大写
标准希腊字母英文对照
alpha,beta,gamma,delta,epsilon,zeta,eta,theta,iota,kappa,lambda,mu,nu,xi,omicron(o),pi,rho,sigma,tau,upsilon,phi,chi,psi,omega
其中的omicron可以简写成o,而且前面无需添加斜杠.
小写希腊字母
大写希腊字母
\变体就是大写的基础上稍稍斜了点
\在希腊字母英文前面加上var,注意一定要让那个字母英文首字母大写,否则会失效.
\vargamma无效 \varGamma有效
泰勒级数公式是将一个函数在某点处展开成无穷级数的一种表示形式,其一般公式为:
[f(x)=∑n=0∞n!f(n)(x0)(x−x0)n]
其中,(f(x))是要展开的函数,(x_0)是展开的中心点,(f^{(n)}(x_0))是(f(x))在(x_0)处的(n)阶导数,(n!)是(n)的阶乘,((x - x_0)^n)是幂次项。
泰勒级数的展开是基于函数在某点的局部近似,通过不断增加级数的项数,可以提高近似的精度。以下是对该公式的详细解释:
泰勒级数的前(n)项和构成了泰勒多项式,它是对原函数的一种近似表示:
[Pn(x)=∑k=0nk!f(k)(x0)(x−x0)k=f(x0)+f′(x0)(x−x0)+2!f′′(x0)(x−x0)2+⋯+n!f(n)(x0)(x−x0)n]
泰勒级数中除了前面的(n)项泰勒多项式外,剩余的部分就是余项(R_n(x)),它表示了用泰勒多项式近似原函数时的误差。余项有多种表示形式,常见的有以下几种:
当(x_0 = 0)时,泰勒级数就变成了麦克劳林级数,其公式为:
[f(x)=∑n=0∞n!f(n)(0)xn=f(0)+f′(0)x+2!f′′(0)x2+⋯+n!f(n)(0)xn+⋯]
以下是一些常见函数的麦克劳林级数展开式:
线性模型通过以下方式扩展核心思想:
机器学习模型分为
有监督学习模型主要分为单模型和集成学习两种:
线性模型:
k近邻:基于实例的学习方法,给定测试样本,通过计算与训练集中样本的距离(如欧氏距离),找出k个最近邻样本,根据这些样本的类别(分类问题)或数值(回归问题)来预测测试样本的结果。
决策树:
神经网络:
支持向量机:
无监督学习模型中的两个主要类别:聚类和降维。
所有的回归算法和分类算法都属于监督学习。并且明确的给给出初始值,在训练集中有特征和标签,并且通过训练获得一个模型,在面对只有特征而没有标签的数据时,能进行预测。
以下是重新制作的表格,更加清晰有序。您可以根据需要进一步调整格式:
| 分类 | 定义 | 算法 | 案例 |
|---|---|---|---|
| 分类 | 对离散随机变量进行建模预测的监督学习 | LR, SVM, KNN, 决策树, 随机森林, GBDT | 垃圾邮件分类 |
| 回归 | 对连续随机变量进行建模预测的监督学习 | 非线性回归, SVR (支持向量回归->可用径向基核 (RBF)), 随机森林 | 房价预测 |
| 聚类 | 基于数据的内部规律,寻找其属于不同族群的无监督学习 | Kmeans, 层次聚类, GMM (高斯混合模型) |
原理: 用线性函数拟合数据,用 MSE(均方差) 计算损失,然后用梯度下降法(GD)找到一组使 MSE 最小的权重。
指分析因变量和自变量之间关系
利用最小二乘函数对一个或多个自变量和因变量之间关系进行建模的一种回归分析.
最小二乘和均方误差之间的关系
不一定,它只是目标函数在当前的点的切平面上下降最快的方向。
在实际执行期中,牛顿方向(考虑海森矩阵)才一般被认为是下降最快的方向,可以达到超线性的收敛速度。梯度下降类的算法的收敛速度一般是线性甚至次线性的(在某些带复杂约束的问题)。
也称为"对数几率回归"。
优点:
LR中最核心的概念是 Sigmoid 函数,Sigmoid函数可以看成LR的激活函数。
Regression 常规步骤:
LR模型属于线性模型,线性模型不能很好处理非线性特征,特征组合可以引入非线性特征,提升模型的表达能力。
另外,基本特征可以认为是全局建模,组合特征更加精细,是个性化建模,但对全局建模会对部分样本有偏,对每一个样本建模又会导致数据爆炸,过拟合,所以基本特征+特征组合兼顾了全局和个性化。
共同点:

梯度下降法,随机梯度下降法,牛顿法,拟牛顿法(LBFGS,BFGS,OWLQN)
目的都是求解某个函数的极小值。
指模型在训练集上的效果很好,在测试集上的预测效果很差.
模型复杂度低或者数据集太小,对模型数据的拟合程度不高,因此模型在训练集上的效果就不好.
将原始dataset划分为两个部分.一部分为训练集用来训练模型,另外一部分作为测试集测试模型效果.
将原始数据集划分为k个子集,将其中一个子集作为验证集,其余k-1个子集作为训练集,如此训练和验证一轮称为一次交叉验证。 交叉验证重复k次,每个子集都做一次验证集,得到k个模型,加权平均k个模型的结果作为评估整体模型的依据。
k越大,不一定效果越好,而且越大的k会加大训练时间; 在选择k时,需要考虑最小化数据集之间的方差,比如对于2分类任务,采用2折交叉验证,即将原始数据集对半分,若此时训练集中都是A类别,验证集中都是B类别,则交叉验证效果会非常差。
一种调优方法,在参数列表中进行穷举搜索,对每种情况进行训练,找到最优的参数。
从超参数空间中随机采样一定数量的组合进行评估,从中选出表现最好的一组。
网格搜索(Grid Search)和随机搜索(Random Search)都是超参数调优 的常用方法,用于在机器学习模型中寻找最优的超参数组合。虽然它们的目标相同,但在实现方式、效率、适用场景等方面有明显区别。
准确率是所有判断正确的样本占所有样本的比例;
精确率是所有判断为正例的样本中真正的正例占所有判断为正例的样本的比例;
召回率则是所有正例中被判断出来的比率。
横轴为召回率(查全率),纵轴为精准率(查准率);
引入“平衡点”(BEP)来度量,表示“查准率=查全率”时的取值,值越大表明分类器性能越好。
准确率和召回率的权衡: 只有在召回率Recall和精确率Precision都高的情况下,F1 score才会很高,比BEP更为常用。
定义: 在损失函数后加上一个正则化项(惩罚项),其实就是常说的结构风险最小化策略,即损失函数 加上正则化。一般模型越复杂,正则化值越大。
正则化项是用来对模型中某些参数进行约束,正则化的一般形式:
第一项是损失函数(经验风险),第二项是正则化项
公式可以看出,加上惩罚项后损失函数的值会增大,要想损失函数最小,惩罚项的值要尽可能的小,模型参数就要尽可能的小,这样就能减小模型参数,使得模型更加简单。
标准化是依照特征矩阵的列处理数据,其通过求z-score的方法,将样本的特征值转换到同一量纲下。归一化是依照特征矩阵的行处理数据,其目的在于样本向量在点乘运算或其他核函数计算相似性时,拥有统一的标准,也就是说都转化为“单位向量”。 归一化的目的是方便比较,可以加快网络的收敛速度;标准化是将数据利用z-score(均值、方差)的方法转化为符合特定分布的数据,方便进行下一步处理,不为比较。
线性回归,逻辑回归,KNN,SVM,神经网络。
主要是因为特征值相差很大时,运用梯度下降,损失等高线是椭圆形,需要进行多次迭代才能达到最优点,如果进行归一化了,那么等高线就是圆形的,促使SGD往原点迭代,从而导致需要迭代次数较少。
定义: 决策树就是一棵树,其中跟节点和内部节点是输入特征的判定条件,叶子结点就是最终结果。
损失函数:正则化的极大似然函数;
目标是 以损失函数为目标函数的最小化。
算法通常是一个 递归的选择最优特征,并根据该特征对训练数据进行分割,使得对各个子数据集有一个最好的分类过程。
决策树量化纯度:
判断数据集“纯”的指标有三个:Gini指数、熵、错误率
背诵:按照基尼指数、信息增益来选择特征,保证划分后纯度尽可能高。
过拟合:选择能够反映业务逻辑的训练集去产生决策树;
剪枝操作(前置剪枝和后置剪枝); K折交叉验证(K-fold CV)
欠拟合:增加树的深度,RF
分为预剪枝和后剪枝,预剪枝是在决策树的构建过程中加入限制,比如控制叶子节点最少的样本个数,提前停止;
后剪枝是在决策树构建完成之后,根据加上正则项的结构风险最小化自下向上进行的剪枝操作.
剪枝的目的就是防止过拟合,是模型在测试数据上变现良好,更加鲁棒.
决策树的优点:
决策树的缺点:
决策树可以表示成给定条件下类的条件概率分布. 决策树中的每一条路径都对应是划分的一个条件概率分布. 每一个叶子节点都是通过多个条件之后的划分空间,在叶子节点中计算每个类的条件概率,必然会倾向于某一个类,即这个类的概率最大.
核心思想与结构
网络拓扑结构
算法流程
| 步骤 | 操作 | 关键公式 |
|---|---|---|
| 1 | 初始化权重 | 随机小值(如[-0.7, 0.7]) |
| 2 | 前向传播 | hk=σ(∑wihxi) |
| 3 | 计算输出误差 | e=21∑(do−yo)2 |
| 4 | 误差反向传播 | \delta_o = (d_o - y_o) \sigma'(yi_o) |
| 5 | 权重更新 | Δwho=ηδohk |
偏导数计算
误差反向传播(链式法则)
权重初始化
输入标准化
过拟合控制技术
局部最小值问题
| 问题类型 | 特征 | 应对策略 |
|---|---|---|
| 局部极小值 | 误差曲面的低谷 | 多随机初始化(5-10次) |
| 鞍点 | 梯度为0的非极小点 | 动量优化(momentum) |
梯度消失问题
隐藏层配置
| 数据复杂度 | 推荐结构 | 说明 |
|---|---|---|
| 简单(线性可分) | 单隐藏层,5-10神经元 | 如XOR问题 |
| 中等(手写数字) | 1-2隐藏层,12-50神经元 | 如ZIP编码数据集 |
| 复杂(图像分类) | 卷积神经网络+全连接 | 使用ReLU |
权重初始化方法
超参数调优
graph TB
A[学习率η] --> B[网格搜索:0.001, 0.01, 0.1]
C[正则系数λ] --> D[交叉验证选择]
E[批量大小] --> F[32/64/128样本/批]
| 组件 | 推荐策略 | 注意事项 |
|---|---|---|
| 隐藏层数 | 1-3层 | 层数增加提升特征层次性 |
| 每层神经元数 | 5-100个 | 输入数据复杂时增加单元数 |
| 输出层 | 回归:线性单元 分类:Softmax |
二分类可用Sigmoid |
💡 误差曲面特性
平坦区:σ(z)≈0 或 1 → 梯度消失
局部极小:随机初始化+多次训练避免陷入
附:激活函数导数特性
Sigmoid导数:\sigma'(z) = \sigma(z)(1-\sigma(z))
考试重点:
① BP算法权重更新公式推导
② 过拟合解决方案对比(早停 vs 正则化)
③ Sigmoid梯度消失原因分析
| 核函数类型 | 公式 | 特点 |
|---|---|---|
| 线性核 | K(xi,xj)=xiTxj | 无参数,适用于线性可分 |
| 多项式核 | (1+xiTxj)d | d 控制复杂度 |
| 高斯核 (RBF) | exp(−γ∣xi−xj∣2) | γ 控制样本影响范围 |
| 概念 | 分类SVM | 回归SVM |
|---|---|---|
| 目标 | 最大化分类间隔 | 最小化 ϵ-带外误差 |
| 损失函数 | 合页损失 (Hinge Loss) | ϵ-不敏感损失 |
| 支持向量 | 边界上的点 & 错分点 | 落在间隔带外的点 |
| 参数影响 | C 控制分类严格性 | ϵ 控制回归带宽度 |
重要概念:
- 必考推导:软间隔SVM对偶问题、KKT条件
- 核函数选择:高斯核参数 γ 的作用(γ↑ → 模型更复杂)
- 回归SVM图解:理解 ξi 和 ξi∗ 对应上/下界误差
重要问题:如何求距离?
代表方法:
主要特点:
需要的预设:k和初始中心点
一般来说,初始中心点是随机生成的,且初始中心点的选取对聚类结果影响很大。
a(xi) 表示该样本与其所在簇中其他样本的平均距离(簇内平均距离)。 b(xi) 表示该样本与相邻簇(距离最近的其他簇)中样本的平均距离。
k-means++是针对k-means中初始质心点选取的优化算法。
流程如下:
bi-kmeans是针对kmeans算法会陷入局部最优的缺陷进行的改进算法。
流程如下:
k-means算法对于凸性数据具有良好的效果,能够根据距离来讲数据分为球状类的簇,但对于非凸形状的数据点,这个时候就需要用到基于密度的聚类方法了
代表方法:
主要特点:
流程如下:
DBSCAN算法关键概念:
在DBSCAN算法中,使用了统一的值,当数据密度不均匀的时候,如果设置了较小的值,则较稀疏的cluster中的节点密度会小于 ,会被认为是边界点而不被用于进一步的扩展;如果设置了较大的 值,则密度较大且离的比较近的cluster容易被划分为同一个cluster。
对于密度不均的数据选取一个合适的是很困难的,对于高维数据,由于维度灾难(Curse of dimensionality),ε的选取将变得更加困难。
前面介绍的几种算法确实可以在较小的复杂度内获取较好的结果,但是这几种算法却存在一个链式效应的现象,比如:A与B相似,B与C相似,那么在聚类的时候便会将A、B、C聚合到一起,但是如果A与C不相似,就会造成聚类误差,严重的时候这个误差可以一直传递下去。为了降低链式效应,这时候层次聚类就该发挥作用了。
簇间相似度度量:
除了需要衡量对象之间的距离之外,有些聚类算法(如层次聚类)还需要衡量cluster之间的距离
称自底向上(bottom-up)的层次聚类,每一个对象最开始都是一个 cluster,每次按一定的准则将最相近的两个 cluster 合并生成一个新的 cluster,如此往复,直至最终所有的对象都属于一个 cluster。这里主要关注此类算法。
过程如下:
又称自顶向下(top-down)的层次聚类,最开始所有的对象均属于一个 cluster,每次按一定的准则将某个 cluster 划分为多个 cluster,如此往复,直至每个对象均是一个 cluster。
| 算法类型 | 适合的数据类型 | 抗噪点性能 | 聚类形状 | 算法效率 |
|---|---|---|---|---|
| kmeans | 混合型 | 较差 | 球形 | 很高 |
| k-means++ | 混合型 | 一般 | 球形 | 较高 |
| bi-kmeans | 混合型 | 一般 | 球形 | 较高 |
| DBSCAN | 数值型 | 较好 | 任意形状 | 一般 |
| OPTICS | 数值型 | 较好 | 任意形状 | 一般 |
| Agglomerative | 混合型 | 较好 | 任意形状 | 较差 |
广义指代所有通过数据训练得到的预测模型
| 名称 | 定义 | 示例 |
|---|---|---|
| 学习器 | 广义指代所有通过数据训练得到的预测模型 | SVM、决策树、神经网络 |
| 基学习器 | 同质集成中的个体学习器 | Bagging中的决策树 |
| 弱学习器 | 性能略优于随机猜测的简单模型(错误率 < 50%) | AdaBoost中的决策树桩 |
| 强学习器 | 高精度模型,或由弱学习器组合而成的集成模型 | 深度神经网络、XGBoost |
学习器的性能受限于:
解决方案:
集成学习可以分为两种:Boosting和Bagging。
| 特征 | Bagging | Boosting |
|---|---|---|
| 核心理念 | 并行生成多个独立模型,降低方差 | 串行生成依赖模型,逐步修正错误,降低偏差 |
| 训练顺序 | 基学习器独立训练,可并行化 | 弱学习器顺序训练,必须串行 |
| 数据使用 | 自助采样(Bootstrap) 每个基学习器用不同子集 |
每轮用全部数据,但调整样本权重 |
| 样本权重 | 所有样本权重相同(均匀采样) | 错误样本权重增加(聚焦难例) |
| 基学习器关系 | 相互独立,无依赖关系 | 强依赖,后序模型修正前序错误 |
| 结合策略 | 分类:投票法 回归:平均法 |
加权投票(准确率高的模型权重更大) |
| 目标效果 | 降低方差 适合高方差模型(如深度决策树) |
降低偏差 适合高偏差模型(如树桩) |
| 过拟合风险 | 不易过拟合(平均抑制噪声) | 易过拟合(需控制迭代次数/学习率) |
| 异常值敏感度 | 不敏感 | 敏感(错误样本权重持续增加) |
| 代表性算法 | 随机森林(Random Forest) | AdaBoost, Gradient Boosting, XGBoost |
| 计算效率 | 高效(可并行) | 较低(串行依赖) |
| 噪声鲁棒性 | 高(平均抑制异常值) | 低(异常值权重被放大) |
| 结果稳定性 | 高(多次运行结果一致) | 中(依赖初始样本权重) |
| 超参数调优 | 简单(通常默认设置即可) | 复杂(需调迭代次数/学习率/正则化) |
选 Bagging 当:
选 Boosting 当:
二者可结合:如 随机森林 + Gradient Boosting(XGBoost/LightGBM)在竞赛中表现优异。
核心思想
集成学习基础
加法模型结构
前向分步算法流程
AdaBoost算法流程
指数损失函数
权重机制
模型形式
训练策略
Gradient Boosting(梯度提升)是一种集成弱学习模型的机器学习方法,例如GBDT就是集成了多个弱决策树模型。
梯度提升框架
适用性优点
Bagging 是Bootstrap Aggregating 的缩写,其算法的基本思想是从原始的数据集中抽取数据,形成K个随机的新训练集,然后训练出K个不同的模型。
目标:降低模型方差(Variance),提升泛化能力
方法:通过 Bootstrap 抽样生成多份训练集,并行训练多个基学习器,通过投票(分类) 或 平均(回归) 结合预测结果。
核心公式:
f^<em>bag(x)=B1∑</em>b=1Bf^∗b(x)
- B:基学习器数量
- f^∗b:第 b 个Bootstrap样本训练的基学习器
设单个基学习器误差方差为 σ2,各基学习器误差相关系数为 ρ,则 Bagging 集成的方差为:
示例:
若单棵决策树测试误差率 40%(σ2=0.4),取 B=100,ρ=0.2:Var≈0.2×0.4+1000.8×0.4=0.0832(误差率≈28.8
Bagging 的扩展变体,引入双重随机性进一步降方差:
多数情况下的Bagging,都是基于决策树的,构造随机森林的第一个步骤其实就是对多棵决策树进行Bagging,我们把它称为树的聚合(Bagging of Tree )。
升维与降维的目的
示例:一维不可分问题
标准K-均值流程
核化改进
非线性SVM的核化
权向量与分类决策
示例(二维不可分数据)
1. **PCA核心思想**
- 将数据投影到方差最大的正交方向(主成分)。
- **协方差矩阵**:Σ=M1∑xixiT。
- **特征方程**:Σv=λv → v 为特征向量(投影方向)。
KPCA的核化步骤
KPCA vs PCA
| 特性 | PCA | KPCA |
|---|---|---|
| 空间 | 原始空间 | 核映射的高维特征空间 |
| 可分离性 | 仅线性可分 | 非线性可分 |
| 计算复杂度 | O(d3) | O(M3)(样本驱动) |
循环神经网络(Recurrent Neural Network,RNN)是一种具有循环连接的神经网络结构,被广泛应用于自然语言处理、语音识别、时序数据分析等任务中。相较于传统神经网络,RNN的主要特点在于它可以处理序列数据,能够捕捉到序列中的时序信息。
RNN的基本单元是一个循环单元(Recurrent Unit),它接收一个输入和一个来自上一个时间步的隐藏状态,并输出当前时间步的隐藏状态。在传统的RNN中,循环单元通常使用tanh或ReLU等激活函数。
离开地=Taipei, 时间=November 2ndarrive Taipei → 目的地 vs leave Taipei → 出发地核心创新:记忆单元 (Memory)
graph LR
A[输入 x_t] --> B[隐藏层 a_t]
B --> C[输出 y_t]
B --> D[记忆单元]
D -->|t+1时刻| B
计算示例
| 时间步 | 记忆输入 | 当前输入 | 新记忆(a_t) | 输出(y_t) |
|---|---|---|---|---|
| t=1 | [0,0] | [1,1] | [1,1] | [1,1] |
| t=2 | [1,1] | [1,1] | [1+1+1, 1+1+1]=[3,3] | [3,3] |
| t=3 | [3,3] | [2,2] | [3+3+2, 3+3+2]=[8,8] | [8,8] |
| 类型 | 记忆来源 | 特点 |
|---|---|---|
| Elman Network | 上一步隐藏层 at−1 | 标准RNN实现 |
| Jordan Network | 上一步输出 yt−1 | 减少梯度传播路径 |
graph LR
subgraph 正向
x1 --> a1 --> y1
a1 --> a2
x2 --> a2 --> y2
end
subgraph 反向
x1 --> a1' --> y1
a1' --> a2'
x2 --> a2' --> y2
end
y1 & y1' --> o1[最终输出t1]
y2 & y2' --> o2[最终输出t2]
graph TB
x_t & a_t-1 --> ForgetGate
x_t & a_t-1 --> InputGate
x_t & a_t-1 --> OutputGate
x_t & a_t-1 --> NewCandidate
ForgetGate -->|f_t| CellState-1
InputGate -->|i_t| NewCandidate
NewCandidate -->|C_tilde_t| CellStateUpdate
CellState-1 --> CellStateUpdate
CellStateUpdate --> CellState_t
CellState_t --> OutputGate
OutputGate -->|o_t| a_t
门控计算(σ为sigmoid):
LSTM的优点:
| 概念 | 核心功能 | 数学工具 |
|---|---|---|
| 记忆单元 | 存储历史状态 | at=f(Waat−1+Wxxt+b) |
| 双向RNN | 同时捕获过去与未来信息 | 正向+反向隐藏层联合输出 |
| LSTM三门 | 遗忘门/输入门/输出门控制信息流 | Sigmoid门控+Tanh变换 |
| 梯度问题解决 | 细胞状态 Ct 的加法更新避免梯度消失 | Ct=ft⊙Ct−1+it⊙C~t |
若一个试验满足如下条件:
这样的试验称为随机试验,简称试验,一般用字母E表示。
随机试验所有可能结果组成的集合称为样本空间(sample space),通常记为Ω,样本空间中的每一个结果称为基本事件(basic event)。
1.样本空间里面所有的元素必须是最基本的,即不可再分。2.样本空间必须是所有可能的基本结果,即具有完备性,且同一个基本结果在样本空间中只出现一次。
样本空间Ω的任一子集称为随机事件(random event),通常用大写字母A、B、C等表示。不包括任一基本事件的事件称为不可能事件,用∅表示;包含样本空间中所有基本事件的事件称为必然事件,用Ω表示。
设 A,B 为两个随机事件,则事件A与事件B 同时发生的事件,称为事件 A,B 的积事件,记为 AB 或 A∩B ,如下图所示。

事件A或事件B发生的事件(即事件A与事件B至少有一个事件发生的事件),称为事件 A,B 的和事件,记为 A+B 或 A∪B ,如下图所示。
事件A发生而事件B不发生的事件,称为事件 A,B 的差事件,记为 A−B ,事件A不发生的事件,称为事件A的补事件,记为 Aˉ。
设 A,B 为两个随机事件,若事件A发生时,事件B一定发生,则称A包含于B ,记为 A⊂B 。若有 A⊂B,B⊂A 称两事件相等,记为 A = B 。
若事件 A 与 B 不能同时发生,称事件 A,B 不相容或互斥,如下图所示。

若事件 A 与 B 不能同时发生,但至少会有一个发生,称事件 A,B 为对立事件,如下图所示。

设随机试验E的样本空间为 Ω,在 Ω 上定义满足如下条件的随机事件的函数P(A)(A⊂Ω) ,称为事件A的概率:
(非负性) 对任意的事件A ,有 P(A)≥0;
(归一性) P(Ω)=1;
(可列可加性) 设 A1,A2,…,An,… 为不相容的随机事件,则有 P(⋃n=1∞An)=∑n=1∞P(An),
则对任意的A⊂Ω ,称 P(A) 为事件 A 的概率。
[ P(A - B) = P(A\overline{B}) = P(A) - P(AB) ]
证明:( A = (A - B) + AB ),且( A - B )与( AB )互斥,根据概率的有限可加性,有( P(A) = P(A - B) + P(AB) ),即( P(A - B) = P(A) - P(AB) )。
又因为( A = A\overline{B} + AB ),且( A\overline{B} )与( AB )互斥,由有限可加性得:( P(A\overline{B}) = P(A) - P(AB) )
证明:( A + B = (A - B) + (B - A) + AB ),且( A - B, B - A, AB )两两互斥,由有限可加性,可得: [ P(A + B) = P(A - B) + P(B - A) + P(AB) ]
再结合减法公式,有: [ P(A + B) = P(A) - P(AB) + P(B) - P(BA) + P(AB) = P(A) + P(B) - P(AB) ]
设( A, B )为两个事件,且( P(A) > 0 ),则 [ P(B|A) = \frac{P(AB)}{P(A)} ]
设( P(A) > 0 ),则 [ P(AB) = P(A)P(B|A) ]
[ P(A_1A_2 \dots A_n) = P(A_1)P(A_2|A_1)P(A_3|A_1A_2) \dots P(A_n|A_1A_2 \dots A_{n-1}) ]
在古典概型(Classical Probability Model)中,样本空间中的每个基本事件发生的概率是相同的。如果样本空间中有n个可能的基本事件,而感兴趣的事件A包含其中的m个基本事件,则事件A发生的概率P(A)可以表示为:
什么是古典概型?1.每个基本事件发生的概率是相等的。2 .样本空间中基本事件的总数是有限的。
将n个球随机分配到N个盒子中
什么是简单随机抽样问题 (Simple Random Sampling)?从总体中随机抽取样本的过程称为随机抽样(Random Sampling)。如果从总体中抽取的每一个样本都具有相同的概率被选中,则称这种抽样方法为简单随机抽样。指从包含N个单位的总体中,不加任何主观选择,完全按照随机原则抽取n个单位组成样本,使得每个单位被抽中的概率相等,且每个可能的样本组合被抽中的机会也相等。
从含有N个球的盒子中进行简单随机抽样
简单随机抽样问题 1.2 几何概型求概率 1.3 重要公式求概率 2 一维随机变量及其分布 2.1 随机变量及其分布函数的定义 离散型随机变量及其概率分布(概率分布) 连续型随机变量及其概率分布(分布函数) 2.2 离散型分布 0-1分布 X ∼ B ( 1 , p ) X \sim B(1,p) X∼B(1,p) 二项分布 X ∼ B ( n , p ) X\sim B(n,p) X∼B(n,p) 负二项分布(帕斯卡分布) X ∼ N b ( r , p ) X\sim Nb(r,p) X∼Nb(r,p) 几何分布 X ∼ G ( p ) X\sim G(p) X∼G(p) 超几何分布 X ∼ H ( n , M , N ) X\sim H(n,M,N) X∼H(n,M,N) 泊松分布 X ∼ P ( λ ) X\sim P(λ) X∼P(λ) 离散型→离散型 2.3 连续型分布 均匀分布 X ∼ U ( a , b ) X\sim U(a,b) X∼U(a,b) 指数分布 X ∼ E ( λ ) X\sim E(λ) X∼E(λ) 正态分布 X ∼ N ( μ , σ 2 ) X\sim N(μ,σ^2) X∼N(μ,σ 2 ) 连续型→离散型 2.4 混合型分布 连续型→连续型(或混合型) 3 多维随机变量及其分布 3.1 定义 3.2 求联合分布 二维均匀分布与二维正态分布 3.3 求边缘分布 3.4 求条件分布 3.5 判独立 3.6 用分布 3.7(离散型,离散型)→离散型 3.8(连续型,连续型)→连续型 分布函数法 卷积公式法(建议用这个) 最值函数的分布 3.10(离散型,连续型)→连续型【全集分解】 3.11 离散型→(离散型,离散型) 3.12 连续型→(离散型,离散型) 3.13 (离散型,离散型)→(离散型,离散型) 3.14 (连续型,连续型)→(离散型,离散型) 3.15 (离散型,连续型)→(离散型,离散型) 4 数字特征 4.1 数学期望 4.2 方差 4.3 亚当-夏娃公式(全期望定理,全方差定理) 4.4 常用分布的期望和方差 4.5 协方差 4.6 相关系数 4.7 独立性与不相关性的判定 4.8 切比雪夫不等式 5 大数定律与中心极限定理 5.1 切比雪夫大数定律(均值依概率收敛到期望) 5.2 伯努利大数定律(频率依概率收敛到概率) 5.3 辛钦大数定律(均值依概率收敛到期望) 5.4 中心极限定理(n足够大时,均收敛于正态分布) 6 统计量及其分布 6.1 统计量 6.2 标准正态分布分布的上α分位数 6.3 卡方分布 X ∼ χ 2 ( n ) X\sim \chi^2(n) X∼χ 2 (n) 6.4 t分布 t ∼ t ( n ) t\sim t(n) t∼t(n) 6.5 F分布 F ∼ F ( n 1 , n 2 ) F\sim F(n_1,n_2) F∼F(n 1 ,n 2 ) 6.6 正态总体下的常用结论 7 参数估计与假设检验 7.1 矩估计 7.2 最大似然估计(MLE) MLE的应用 7.3 常见分布的矩估计量和最大似然估计量 7.4 无偏性:求期望 7.5 有效性:比方差,方差越小越有效 7.6 一致性(相合性):大数定律 7.7 区间估计 7.8 假设检验 选择检验统计量 7.9 两类错误 第一类错误:弃真(直接算落入拒绝域的概率) 第二类错误:取伪(直接算落入收敛域的概率)
[TOC]
Typora是一种文本编辑器,可用来方便地编辑Markdown!
Markdown是一种轻量级标记语言,或者说是一种简单的格式化文本的方法,在任何设备上看起来都很棒.不会做任何花哨的事情,比如改变字体大小,颜色或类型——只是基本的,使用你已经知道的键盘符号.
Typora可以将.md 导出成多种文件,如.pdf ,html, .docx
世界上最流行的博客平台WordPress和大型CMS如Joomla,Drupal都能很好的支持Markdown.完全采用Markdown编辑器的博客平台有Ghost和Typecho等.
# 一级标题
## 二级标题
### 三级标题
#### 四级标题
标题会在目录和大纲中分级显示,点击可以跳转.
在Typora中建议开启严格模式,即不应该是#标题,应该为# 标题
手动补上空格,使得Markdown语法在其他文本编辑器上兼容.
**欢迎报考zjut**
__欢迎报考浙江大学__
或者选中想要强调的文字按下ctrl+B
E.G.
欢迎报考zjut
*欢迎大佬来浇灌我各种知识*
_欢迎打扰大佬来浇灌我各种知识_
或者选中想要强调的文字按下ctrl+I
E.G.
欢迎大佬来浇灌我各种知识
(p.s.斜体并强调[用"***"或"___"包围])
~~我宣布我喜欢XXX~~
E.G.
我宣布个事,我是XXX
(注意:此为拓展语法)
==我想拿绩点第一==
E.G.
==我想拿绩点第一==
`abc`
abc
int main()
{
int num1 = 0;
int num2 = 0;//初始化变量
scanf("%d %d", &num1, &num2);//&表示取地址,scanf表示扫描输入
int sum = num1 + num2;
printf("%d\n", sum);
return 0;
}
记得在```之后加上语言
> 19岁,大一
> > 19岁的大学生该有的特性
引用是可以嵌套的
E.G.
19岁,大一
学生特有的无处不在
- 拉格朗日中值定理
+ 费马引理
* 罗尔定理
敲回车会自动换行,敲下回车后按下tab键会缩进一级
E.G.
我来这里就是为了三件事:
1. 公平
2. 公平
3. 还是tm的公平!
我来这里就是为了三件事:
(注意:为拓展语法)
c语言中int的上限是2^31^-1=2147483647
E.G.
c语言中int的上限是2^31^-1=2147483647
(注意:为拓展语法)
H~2~o
E.G.
H2o是剧毒的
(注意:为拓展语法)
> 今日我们相聚于此,是为了学习Markdown的使用[^1]
[^1]:从xxx引用
需要在文末写上注释对应的内容
E.G.
今日我们相聚于此,是为了学习Markdown的使用^1
(写在里原文远一点的地方)
(注意:文内跳转为扩展用法)
[来看看我贫瘠的仓库吧](https://github.com/NNLXLDG/NNLXLDG)
[Typora使用入门:6.code代码](#6.Code代码[用"`"包围])
支持网页链接与文内跳转,按住ctrl并单击鼠标左键即可跳转
E.G.
TODOLIST:
- [ ] 刷牙洗脸
- [ ] 吃早餐
- [x] 起床
E.G.TODOLIST:
也可以插入表格
| 学号 | 姓名 | 年龄 |
|---|---|---|
| 学号 | 姓名 | 年龄 |
原生支持的表格法
| 学号 | 姓名 | 年龄 |
|---|---|---|
| 114514 | 苗所 | 24 |
| 1919810 | 浩三 | 25 |

E.G.
From Bilibili 8KRAW 眼界摄影大赛
鲸落
***
---(最方便)
___
//(其实按三个及以上都可以)
E.G.
(注意:英语输入为拓展语法)
:sweat_smile:
:drooling_face:
:clown_face:
//(敲回车或者鼠标点击,后置的":"一般不需要手动输入)
对英文要求较高
:sweat_smile: :drooling_face: :clown_face:
对于其他普通的Markdown文本编辑器是会失效的,但我们可以直接将Emoji表情复制进来,这是直接硬编码的
用好这个功能可以让你的文本变得可爱,
分享一个全emoji网站
[TOC](此为Typora特有的,如本文档开头)
若使用VS Code 搭配Markdown All in One扩展,可在VS Code 的命令面板(即VS Code Command Palette)输入Create Table of Contents 自动生成目录,且可在扩展设置中细调目录参数.
预留
只要你会写,你完全可以把Markdown当作HTML来写.
同时,.md文件可以直接导出成一个网页.
下划线可以选中想要下划的文字按下ctrl+u
(注意:部分编译器会不识别部分符号)
用</code>包围为单条公式,按下两个<code>并敲回车即生成公式块
x2+y2=1
| 按键 | 效果 | 按键 | 效果 | |
|---|---|---|---|---|
ctrl+D |
选中当前词 | ctrl+L |
选中当前句/行 | |
ctrl+E |
选中当前区块 | ctrl+F |
搜索当前选中 | |
ctrl+H |
替换当前选中 | ctrl+K |
将当前选中生成链接 | |
ctrl+回车 |
表格下方插入行 | ctrl+, |
打开偏好设置 | |
ctrl+/ |
切换正常/源代码视图 | ctrl+shift+- |
缩小视图缩放 | |
ctrl+shift++ |
增大视图缩放 | ctrl+M(vsc中) |
自动插入公式 $ | |
ctrl+M(vsc中)连按两下 |
自动生成上下两个$$ | alt+shift+F |
表格格式化 |
23智能科学实验班-谢许康-302023568034
不变的值称为常量,可变的值称为变量
类型是用来创建变量的。
data_type + name
char ch = 'c'
int weight = 148
int salary = 30000
double price = 33.3
变量分为局部变量和全局变量
{}外面的变量称为全局变量, {}内部的变量称为局部变量.
包括在另一个文件里面的,也算全局变量,但是在使用之前,要声明外部变量,使用extern+数据类型+名字来声明变量
int a = 100;
int main()
{
int a = 10;
printf("a=%d\n", a);
return 0;
}
输出结果为10,
当全局变量与局部变量冲突时,局部优先,没有局部,只能全局。应避免局部变量和全局变量取一样的名。
int main()
{
int num1 = 0;
int num2 = 0;
scanf("%d %d", &num1, &num2);
int sum = num1 + num2;
printf("%d\n", sum);
return 0;
}
&表示取地址,scanf表示扫描输入
变量的作用域(scope)是程序设计概念,通常来说,一段程序代码中所有用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域.
局部变量的作用域
局部变量的作用域是变量所在的局部范围,大包小
//这是一个错误示范
int main()
{
{
int a = 12;
printf("a=%d\n", a);
}
printf("a=%d\n", a);//报错
return 0;
}
局部变量只能在局部生效,第二个printf("a=%d\n", a)无法输出。
int main()
{
int a = 12;
{
printf("a=%d\n", a);
}
printf("a=%d\n", a);
return 0;
}
两个a都可正常输出,因为int a = 12;对外面那个{}以内均适用
全局变量的作用域 全局可用,extern可用于外部,作用域是整个工程
int a = 12;
int main()
{
{
printf("a=%d\n", a);
}
printf("a=%d\n", a);
return 0;
}
两个a的值都可以正常输出
演示全局变量的作用域
int a = 10;
void test()
{
printf("test-->%d\n", a);
}
int main()
{
test();
{
printf("a=%d\n", a);
}
printf("a=%d\n",a);
return 0;
}
三个a的值都可正确输出
自定义函数中也可使用
声明外部变量
extern int a;
(非静态)局部变量的生命周期
进入作用域到出作用域
全局变量的生命周期
整个程序的开始到结束
C语言变量的存储有两种方式:静态存储方式和动态存储方式,相应的生产期也有两种:静态生存期和自动生存期。
所有的全局变量都是静态存储方式。,而局部变量要根据存储类型来区分具体的存储方式。
内存中的三个区域:栈区、堆区、静态区。
三种储存类型:自动变量、静态局部变量、寄存器变量
自动变量的定义方式:
auto <数据类型> <变量名>
auto可省略,没有声明储存类型的局部变量,一律默认为自动变量,平时在函数中看到的没有auto修饰的数据类型名+变量名都是自动变量。
只有在局部变量的作用域,自动变量才能起作用。在程序进入作用域,编译系统自动为其提供内存;在离开作用域后,回收其内存。
#include <stdio.h>
int max(int x,int y) //主函数调用该函数进行传参时,为x,y分配内存,该过程叫做“形参实例化”,函数调用完成后会自动销毁
{
int z; //执行至此,为z分配内存
if(x > y)
z = x;
else
z = y;
return z; //执行至此,回收x,y,z的内存
}
int main()
{
int a, b, c; //执行至此,为a,b,c分配内存
scanf("%d%d", &a, &b);
c = max(a,b);
printf("max=%d\n",c);//执行至此,回收a,b,c的内存
return 0;
}
分析下面变量的创建销毁过程
#include <stdio.h>
int fac(int n)
{
int f = 10;
f = f * n;
return (f);
}
int main()
{
int s;
s = fac(2);
printf("第一次调用的s:%d\n",s);
s = fac(3);
printf("第二次调用的s:%d\n",s);
return 0;
}
定义形式:
static <数据类型> <变量名>
分析下面变量的创建销毁过程
#include <stdio.h>
int fac(int n)
{
static int f = 10;
f = f * n;
return (f);
}
int main()
{
int s;
s = fac(2);
printf("第一次调用的s:%d\n",s);
s = fac(3);
printf("第二次调用的s:%d\n",s);
return 0;
}
static修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变了变量的存储类型,本来⼀个局部变量是存储在内存的栈区的,但是被 static 修饰后存储到了静态区。存储在静态区的变量和全局变量是⼀样的,⽣命周期就和程序的⽣命周期⼀样了,只有程序结束,变量才销毁,内存才回收。但是作⽤域不变的。
生命周期≠作用域(现场解释一下)
寄存器变量是存储在CPU寄存器的自动变量,便于快速访问。
寄存器变量的定义方式:
register 数据类型 变量名
注意:由于寄存器变量存储在CPU的寄存器中,不存储在内存单元中,无法进行取地址运算。
static 和 extern 都是C语⾔中的关键字。
static 可以
extern 是⽤来声明外部符号的。
代码一:
add.c
int g_val = 2018;
test.c
#include <stdio.h>
extern int g_val;
int main()
{
printf("%d\n", g_val);
return 0;
}
代码二: add.c
static int g_val = 2018;
test.c
#include <stdio.h>
extern int g_val;
int main()
{
printf("%d\n", g_val);
return 0;
}
extern 是⽤来声明外部符号的,如果⼀个全局的符号在A⽂件中定义的,在B⽂件中想使⽤,就可以使⽤ extern 进⾏声明,然后使⽤。
代码1正常,代码2在编译的时候会出现链接性错误。
结论:
编写大型工程时,往往会把自定义函数放在另外一个源文件中,这样会更加清晰.
代码1(无static修饰)
//add.c
int Add(int x, int y)
{
return x+y;
}
//test.c
extern int Add(int x, int y);
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
代码2(有static修饰)
//add.c
static int Add(int x, int y)
{
return x+y;
}
//test.c
extern int Add(int x, int y);
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
代码1正常,代码2在编译的时候会出现连接性错误.
命名空间是ANSI C++引入的可以由用户命名的作用域,用来处理程序中常见的同名冲突。
在c++中有4层次的作用域:文件、函数、类、复合语句。在不同的作用域中可以定义名字相同的变量,互不干扰,便于系统区别他们。
如下:
class A
{
public:
void fun1();
private:
int i;
};
void A::fun1()
{
}
class B
{
public:
void fun1();
private:
int i;
};
void B::fun1()
{
}
这样,他们就不会发生混淆。
但是,一个大型的应用软件,往往不是由一个人独立完成的,而是由若干不同的人合作完成的,不同的人分别完成不同的部分,最后组成一个完整的程序。假如不同的人分别定义了类,放在了不同的文件中,在主函数的文件中需要使用这些类时,就用#include指令将这些头文件包含进来。由于头文件是由不同的人设计的,有可能在不同头文件中用了相同的名字来命名所定义的类或函数。这样,程序中就会出现名字冲突。
以下面的程序为例,在People A.h和People B.h分别定义类和函数:
//PeopleA.h
class Student
{
public:
Student(int n, char s, string name)
{
//.....
}
private:
int num;
string name;
char sex;
};
int fun(int a, int b)
{
return a + b;
}
//PeopleB.h
class Student
{
public:
Student(int n, char s, string name)
{
//.....
}
private:
int num;
char sex;
string name;
};
int fun(int a, int b)
{
return a + b;
}
假如在主程序中要用到People A.h中的Student函数,需要在头文件中包含People A.h,同时要用到People B.h中的Student函数,需要在头文件中包含People B.h,如果主程序如下:
#include <iostream>
#include "People A.h"
#include "People B.h"
int main()
{
Student stdu1(101, 18, "wang");
cout << fun(5, 3) << endl;
return 0;
}
这时程序就会出错,因为在预编译后,头文件中的 内容取代了对应的#include指令,这样就在同一个程序文件中出现了两个Student类和两个fun函数,显然是重复定义,这就是名字冲突,即在同一个作用域中有两个或者多个同名的实体。
所谓命名空间,实际上就是一个由程序设计者命名的内存区域。程序设计者可以根据需要制定一些有名字的空间域,把一些全局实体分别放在各个命名空间中,从而与其他全局实体分隔开来。如:
namespace AA
{
int a;
double b;
}
namespace是定义命名空间锁必须写的关键字,AA是自己制定的命名空间的名字。如果在程序中要使用a和b,必须加上命名空间名和作用域分辨符::,如AA::a,AA::b,这种用法称为命名空间限定。
命名空间的作用是建立一些互相分隔的作用域,把一些全局实体分隔开来,以免产生名字冲突。
如下程序为例:
//PeopleA.h
namespace PeopleA
class Student
{
public:
Student(int n, char s, string name)
{
//.....
}
private:
int num;
string name;
char sex;
};
int fun(int a, int b)
{
return a + b;
}
//PeopleB.h
namespace PeopleB
class Student
{
public:
Student(int n, char s, string name)
{
//.....
}
private:
int num;
char sex;
string name;
};
int fun(int a, int b)
{
return a + b;
}
#include <iostream>
#include "People A.h"
#include "People B.h"
int main()
{
PeopleA::Student stdu1(101, 18, "wang");
cout << PeopleA::fun(5, 3) << endl;
PeopleB::Student stdu1(101, 18, "wang");
cout << PeopleB::fun(5, 3) << endl;
return 0;
}
在引用命名空间成员时,要用命名空间名和作用域分辨符对命名空间成员进行限定,以区别不同的命名空间中的同名标识符。
即:命名空间名::命名空间成员名
c++提供了一些机制,能简化使用命名空间的使用:
可以为命名空间起一个别名,用来替代较长的命名空间名,如:
namespace PeopleA
可以用一个较短的别名替代它。如:
namespace PA = PeopleA
using后面的命名空间成员名必须是由命名空间限定的名字,如:
using AA::i;
如:using namespace AA;
声明了在本作用域中要用到命名空间AA中的成员,在使用该命名空间的任何成员时都不必再使用命名空间限定。
c++中可以声明无名的命名空间,如:
namespace
{
void fun()
{
//....
}
}
由于命名空间没有名字,在其他文件中显然无法引用,它只在本文件的作用域有效。若无名命名空间的成员fun函数的作用域为文件A,在文件A中使用无名命名空间的成员,不用也无法用命名空间名限定。
标准C++库中的所有标识符都是在一个名为std的命名空间中定义的,或者说标准头文件中的函数、类、对象和模板实在命名空间std中定义的。一般用using namespace语句对命名空间std进行声明,这样可以不必对每个命名空间成员一一进行处理,在文件的开头加入如下语句:
using namespace std;
这样,在std中定义和声明的所有标识符在本文件中都可以作为全局变量来使用。
由于namespace的概念,使用C++标准程序库的任何标识符时,可以有三种选择:
std::cout << std::hex << 3.4 << std::endl;
using std::cout;
using std::endl;
//以上程序可以写成
cout << std::hex << 3.4 << endl;
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
cout << hex << 3.4 << endl;
在所有的计算机程序中,一个基本的目标是操作一些数据,然后获得一些结果。为了操作这些数据,需要为这些数据分配一段内存,我们可以将这段内存称为变量。为了方便操作,以及程序可读性方面的考虑,需要使用一个有意义的名称来引用这段内存,这个名称就是变量名。
将名称和一段内存关联起来的工作可以分成两个阶段来进行,分别是变量的声明和定义。在变量声明的时候,只是引入了一个名称,该名称并没有和一段特定的内存关联。也就是说,在声明变量的时候,只是引入了一个助记符,并没有执行内存分配。在定义变量的时候,将前面声明过程中引入的名称关联到了一段特定的内存,内存的大小由变量的类型决定。也就是说,在定义变量的时候,真正执行了内存分配。在有的情况下,变量的声明和定义是需要分开进行的,如:全局变量的声明和定义,可以在多个文件中使用该变量;而在某些情况下,使用一个语句就可以完成变量的声明和定义,如:局部变量的声明和定义。只需要在一个文件中使用该变量。
在C++程序中,当声明并定义了一个变量以后,需要关注如下两个问题:
为了解决这两个问题,就需要引入作用域的概念。作用域是C++程序中的一段区域,一般用正反两个花括号来界定它的范围。**在同一个作用域范围内,一个名称只能唯一关联到一个实体,这个实体可以是变量,函数,类型,模版等。**也就是说,在同一作用域范围内,不同的实体必须对应不同的名称,绝对不允许出现两个不同的实体对应同一个相同的名称的情况。一个名称可以和不同作用域中的不同实体相对应。也就是说,对于同一个名称,在不同的作用域中可以重复使用。
在本文的后续部分,将对各种类型的作用域进行描述,并且介绍在作用域中进行名字解析的规则。
我们可以将整个C++程序(在程序中包括各种类型,函数,模版,变量等,并且分布在很多个*.cpp文件中)看成一个很大的整体区域。为了方便对C++程序中已经定义的各种类型,函数,模版,变量的管理,可以把这片大的区域划分成一片片小的命名区段。然后根据各个类型,函数,模版,变量的功能以及用途等,再把这些类型,函数,模版,变量等分别放置在不同的区段中。这些小的区段叫做作用域,C++程序支持四种形式的作用域,分别是:名字空间作用域,类域,局部作用域,语句作用域。
名字空间作用域就是程序员利用名字空间定义在C++程序中划分出来的一块比较大的程序区段。在该程序区段内部,可以定义类型,函数,模版,变量。名字空间作用域可以跨越多个*.cpp文件而存在。在名字空间作用域内部还可以继续定义其他的名字空间作用域,也就是说,名字空间作用域是可以互相嵌套的。
全局作用域是C++程序最外层的名字空间作用域,也是最大的名字空间作用域。**全局作用域天然存在于C++程序中,它不需要由程序员人为地定义。在全局作用域内部,可以包含其他的,由程序员定义的名字空间作用域,以及没有包含在其他名字空间作用域中的类型,函数,模版,变量。**在全局作用域中定义的变量是全局变量,在全局作用域中定义的函数是全局函数。
在C++程序中,每定义一个类就会引入一个类域。类体所包含的范围就是类域的范围,在类中定义的所有成员都属于该类域。类域位于名字空间作用域内部,该名字空间作用域可能是全局作用域,也可能是用户定义的名字空间作用域。
**每一个函数体内部都是一个局部作用域。该作用域起始于函数体的左花括号“{”,结束于函数体的右花括号“}”。**每一个函数都有一个独立的局部作用域。在局部作用域内定义的变量都是局部变量。
在C++程序中,当要求使用单个语句,但程序逻辑却需要不止一个单个语句的时候,我们可以使用复合语句。复合语句通常被称为块,是用花括号括起来的一些单个语句的集合。在复合语句花括号内部的区段也属于局部作用域。
有些语句存在控制结构,并且允许在控制结构中定义变量。如:
//示例一:
for ( int K = 0; K < 100;K++ )
cout << K; //该行语句属于语句作用域范围,K仅在这一行有效。
//示例二:
for (int K = 0; K < 100;K++)
{
… //其他代码
Cout << k; //花括号内部是复合语句,都属于语句作用域。K在整个花括号内有效。
… //其他代码
}
从控制语句的开始到控制语句结束这一段区域被称为语句作用域。在该控制结构中定义的变量,仅在该语句作用域内有效。如:示例二中,K在花括号内有效,或者示例一中,仅在语句“cout << K;”中有效。语句作用域是最小的作用域。
使用名字空间可以在一定程度上解决命名冲突的问题。假设没有名字空间,那么在C++程序中,所有的实体,如:函数,类型,变量,模版等,都必须被放置在全局域中作为全局实体而出现。在全局域中,这些实体必须具有唯一的名称,不允许存在多个实体同名的情况。因此,当在全局域中引入一些第三方开发的类库的时候,必须要保证第三方类库中命名的实体与全局域中命名的实体在命名方面不冲突。但是,这是很难保证的。为了解决这个问题,就引入了名字空间的概念。
第三方开发方在开发类库的时候,可以首先声明一个名字空间,每一个用户声明的名称空间都代表一个不同的名字空间域。在该名字空间中,可以包含嵌套其他的名称空间,以及函数,类型,变量,模版等的声明和定义。在该名称空间内部声明的实体被称为名称空间成员。用户在名字空间中声明的每个实体的名字必须是唯一的,不允许重名。因为在不同用户声明的名字空间中引入了不同的域,所以在这些由不同用户声明的名字空间中可以使用相同的名称。通过这种方式解决了命名冲突的问题。
在使用名字空间中的成员的时候,名字空间成员的名字会自动与该名字空间重合,或者说被其限定修饰。如:在名字空间A中声明的类B,它的名字是:A::B。
用户声明的名字空间以namespace关键字开头,后面是名字空间的名称。名字空间的范围以花括号界定,具体的格式如下:
namespace mySpace //mySpace是名字空间的名称
{
Class myClass { … }; //类定义
Int myFunction(int para1,int para2); //函数的声明
Extern double myVar; //变量的声明
}
在上面的示例中,声明了一个名称为mySpace的名字空间,该名字空间的作用域由花括号界定,在花括号内部的部分都属于该名字空间的作用域。在该名字空间中,定义了一个类:myClass,声明了一个函数:myFunction,以及一个变量myVar。它们都是该名字空间的成员。
用户声明的名字空间可以位于全局作用域中,也可以位于其他的名字空间的作用域中。在当前的作用域中,名字空间的名称是唯一的,不能与其类型的实体重名。
**在同一个作用域中,可以多次声明相同名称的名字空间。**在这种情况下,将会实现名字空间的累加。比如,A.h头文件和A.cpp源文件都位于全局作用域中,在这两个文件中分别声明如下的名字空间:
//A. h文件的代码实现:
namespace mySpace //在这里实现了函数和变量的声明,属于接口部分。
{
Int AddData (int para1,int para2); //函数的声明
Extern double myVar; //变量的声明
}
//B.cpp文件的代码实现:
Include “A.h”
namespace mySpace // 在这里实现了函数和变量的定义,属于实现部分。
{
Int AddData(int Para1,int Para2) //函数的定义
{
Return Para1+Para2;
}
Double myVar = 3.14; //变量的定义,并初始化。
}
在这里,存在这样一个规则:在同一个作用域中,如果新声明的一个名字空间的名称与前面声明过的名字空间的名称同名,那么这个后声明的名字空间就是前面声明的名字空间的累加,这两部分内容属于同一个名字空间;如果新声明的这个名字空间不与当前作用域中任何名字空间同名,那么就会定义一个新的名字空间。
在上面的示例中,A.h和A.cpp文件位于全局作用域中。在全局作用域中,两次声明的名字空间具有相同的名称:mySpace。因此,认为这两次声明的名字空间属于同一个名字空间。
通过对上面所描述的规则的使用,在程序设计的时候,可以根据需要,将名字空间的声明拆分成若干个部分来实现,只要这几个部分的声明都在同一个作用域中即可。这个规则的一个典型应用就是:实现接口和具体实现的分离。
在上面的示例中,我们将函数AddData和变量myVar的声明放在了A.h头文件中,而将它们的定义放在了另外一个A.cpp的源文件中。 A.h头文件实现的是函数库的接口的,而A.cpp文件中的内容则是针对接口的实现。因此,在程序设计和开发的时候,这两部分内容可以分别由不同的人在不同的时间实现。通过这种方式,实现了接口和具体实现分离的原则。
当定义了名字空间以后,就可以想名字空间中添加成员。这些被添加的成员可以是:类型,函数,变量,模版等。可以通过两种方式向名字空间中添加成员。
//方式一:在名字空间中直接完成成员的定义。成员的定义不在划分为声明和定义两部分。
Namespace mySpace
{
Double myVar = 3.14;
Int myFunction(int Para1)
{
Return Para1*10;
}
}
//方式二:在名字空间中先完成成员的声明,然后采用名字空间累加的方式,在其他部分完成成员的定义。这个“其他部分”,可以是其他的物理文件,也可以是同一个物理文件。
Namespace mySpace
{
Extern double myVar;
Int myFunction(int Para1);
}
Namespace mySpace
{
Double myVar = 3.14;
Int myFunction(int Para1)
{
Return Para1*10;
}
}
在上面的代码中,在定义了名字空间的同时(无论是采用累加方式,还是一次性完成),在名字空间内部完成了函数myFunction和变量myVar的定义。名字空间的定义和名字空间成员的定义同步完成。
//首先在一个文件中完成名字空间的定义,以及名字空间成员的声明。一般情况下,该文件为头文件(A.h)。
Namespace mySpace
{
Class myClass {….};//声明一个类型
myClass myFunction(myClass Para1);//声明一个函数,该函数返回myClass类型,并以myClass类型为参数。
}
在上面的代码中,完成了对名字空间mySpace的定义,同时在名字空间内部,完成了类myClass的定义,以及对函数myFunction的声明。接下来需要在其他地方,名字空间以外,完成对名字空间成员myFunction函数的定义。具体代码如下:
//实现函数myFunction定义的位置,可以是另外一个文件,一般为cpp文件,但是也可以在原来的头文件中(一般不会这么干)。
#include “A.h”
mySpace::myClass mySpace::myFunction(myClass Para1)
{
//下面完成函数的具体实现。
…
}
在上面的代码中,我们可以看到两处差异。一处是函数的返回值类型,myClass被名字空间mySpace限定修饰了;而在函数的参数类型处,myClass直接使用,没有被名字空间mySpace限定修饰。
这里存在这样一个规则:在函数的限定修饰名称“mySpace::myFunction”之后,直到方括号结束的区域都属于mySpace名字空间的作用域范围。也就是上面代码中的红色部分。
也就是说名字空间的作用域可能会有两部分组成,在大多数情况下,名字空间的作用域是由定义名字空间的时候,名字空间体的花括号界定的。但是,当在名字空间之外定义名称空间的成员的时候,在名字空间成员的限定修饰名之后直到结束花括号(” }”),或者分号(;)的部分都属于该名字空间作用域范围。
因此,在上面的代码中,参数的类型不需要被限定修饰,因为那个区域是属于名字空间作用域内的;而函数的返回类型必须要被限定修饰,因为那个区域不属于名字空间的作用域内。
另外还需要注意,在名字空间之外实现名字空间成员的定义的时候,要有一个前提,那就是:名字空间成员的声明必须在名字空间之内实现。
在C++程序中,使用名字空间的方式封装一些函数库或者类库的时候,一般情况下,通常的做法是这样的:**首先在一个头文件中定义一个名字空间,然后在该名字空间的定义中声明所有的名字空间成员,如:函数,类型,变量等。**之后将这个头文件引入到一个cpp文件中,并且在这个cpp文件中实现所有名字空间成员的定义。具体示例如下:
-----------------A.h------------------------------//头文件名称
namespace myCPlusPlusFunctionsV1.0
{
Class myClass { …//类成员的声明 }; //定义一个类型
Extern double myVar; //声明变量
Void DealClass(myClass*); //声明函数
}
-----------------A.cpp--------------------------//源文件
#include “A.h”
Namespace myCPlusPlusFunctionsV1.0
{
myClass:: myClass() { … // myClass构造函数的实现}
…
//其他myClass类成员的定义。
…
double myVar = 3.14;//变量的定义
void DealClass(myClass*pClass)
{
…//函数的具体实现。
}
}
在使用这些函数库或者类库的时候,首先需要将这个定义了该名字空间的头文件引入,然后开始使用该名字空间中的一些成员。在使用名字空间成员的时候,有三种方式:
------------------otherCPlusPlusFile.cpp-------------------------
#include “A.h”
Void main()
{
myCPlusPlusFunctionsV1.0::myClass *pClass = new myCPlusPlusFunctionsV1.0::myClass;
myCPlusPlusFunctionsV1.01::DealClass(pClass);
}
在上面的代码中,“::”是域操作符。名字空间成员的声明被隐藏在名字空间之中,所以,名称空间的成员名称不会与当前作用域中的对象实体名称产生冲突。在使用名字空间成员的时候,可以使用名字空间名+域操作符+名字空间成员名称的方式将名字空间成员引入到当前的作用域中。否则,在当前作用域中,编译器不会找到名字空间的成员。
域操作符也可以被用来引用全局作用域的成员。因为全局作用域没有名称,所以使用如下的符号:
::member_name
指向全局名字空间的成员。当全局名字空间成员的名称被局部作用域中的名字隐藏的时候,但又需要在局部作用域中使用全局成员的时候,就可以使用这种引用方式。
在上面的示例中,名字空间的名称“myCPlusPlusFunctionsV1.0”比较长,在使用的时候,可能会不方便,因此,C++在处理这个问题的时候,引入了名字空间别名的概念。
所谓名字空间别名就是为已经定义的名字空间取一个其他的、替代性的名称,一帮情况下,这个名称是简短的,容易记忆的。具体使用方式如下:
------------------otherCPlusPlusFile.cpp-------------------------
#include “A.h”
Namespace myC++ = myCPlusPlusFunctionsV1.0;
Void main()
{
myC++::myClass *pClass = new myC++::myClass;
myC++::DealClass(pClass);
}
在上面的代码中,为名字空间“myCPlusPlusFunctionsV1.0”定义了一个别名“myC++”。之后在引用该名字空间成员的时候,就可以使用该别名。
定义名字空间别名的格式是:以关键字namespace开头,后跟名字空间的别名,并且等于前面定义好的名字空间的名称。
Using 声明的作用是:使一个名字空间成员在当前作用域中可见,可见的范围是从using声明的语句开始,直到当前作用域结束。如果在using声明语句之后,在当前作用域中又嵌套了其他的作用域,那么using声明在当前作用域中的嵌套作用域中也同样有效。
Using声明以关键字using开头,后跟名字空间的成员名称。该成员名称必须是名字空间名称+域操作符+名字空间成员名称形式的限定修饰名称。具体代码如下:
//名字空间的定义
Namespace mySpace
{
Int myFunction(int Para)//在名字空间中定义了一个函数
{
Return Para*10;
}
}
//在全局作用域中使用using声明,将名字空间成员名引入当前作用域。
Using mySpace::myFunction;
//开始使用名字空间的成员
Void main()
{
//也可以在此位置使用using声明,即在局部作用域使用using声明。
myFunction(10);//使用名字空间的成员。因为使用了using声明,所以不需要使用限定修饰的形式。名称myFunction从using声明开始,直到当前作用域结束。
}
在上面的代码中,首先定义了一个名字空间,并在名字空间中定义了一个函数。然后在全局作用域中使用了using声明。之后,在main函数中使用名字空间的成员函数myFucntin。
**可以在全局作用域,名字空间作用域,局部作用域中使用using声明。**在使用了using 声明以后,一次只能从源名字空间向当前作用域中引入一个名字空间成员,但可以多次使用using声明。如果该名字空间成员是函数,并且在该名字空间中具有多个重载,那么在使用using声明的时候,所有的重载函数都会被引入到当前的作用域中。**被引入的名字空间成员名只在当前作用域中有效,并且名称唯一。这个被引入的名字空间成员名会隐藏当前作用域外围作用域中的同名名称,也会被当前作用域的嵌套作用域中的同名名称隐藏。**具体情况见如下代码:
namespace mySpace
{
Int myIntVar = 10;//定义一个整型变量。名字空间成员。
}
Int myIntVar = 100;//全局变量
Int main()
{
Using mySpace::myIntVar;//该using声明隐藏了全局变量myIntVar。
Int k = 10;
K = k + myIntVar;//使用的是名字空间的成员变量,所以k的值等于20.
K = K + ::myIntVar;//这里使用的是全局变量,所以k的值等于110.
{
Int myIntVar = 50;//在此语句作用域中声明的变量隐藏了前面using声明中引入的变量。
Int a = myIntVar ;//a = 50
Int b = ::myIntVar;//b = 100;
Int C = mySpace::myIntVar;//c = 10;
}
}
使用using声明将名字空间的成员引入到当前作用域的时候,除了重载函数以外,被引入的成员名称不能与当前作用域中定义的对象实体重名,否则会引起错误。
Using指示符以关键字using 开头后跟关键字namespace,最后是名字空间的名称。该名字空间的名称必须在前面已经定义。其作用域从using指示符开始,直到当前作用域结束。使用using指示符以后,将会把名字空间中的所有成员引入到当前作用域。具体的代码如下:
//定义名字空间
Namespace mySpace
{
Int myFunction(int Para)
{
Return Para*10;
}
Int myVar = 100;
}
//使用using指示符,将名字空间的所有成员引入到当前作用域。目前是全局作用域。
Using namespace mySpace;
Void main()
{
Int k = myVar + 10;//使用using指示符以后,可以直接使用名字空间中的成员,就好像该//名字空间的成员在当前作用域中定义的一样,不需要限定修饰。
myFunction(k);
}
在上面的代码中,首先定义了一个名字空间mySpace,同时在名字空间中定义了一个函数myFunction,以及一个变量myVar。然后使用using指示符将该名字空间中的成员引入到了全局作用域中。之后,在main函数中使用名字空间的成员,使用的时候,不需要限定修饰,就好像使用当前名字空间中定义的成员一样。
在当前作用域使用using指示符以后,被引用的名字空间将与当前的作用域合并,名字空间中的成员就好像在当前作用域被定义一样。因此,在当前作用域中,不能定义与名称空间成员重名的对象。否则会因此错误。
在名字空间的概念被提出之前,在C++中就已经存在了大量的库函数。这些库函数有的是标注C形式的,也有的是标准C++形式的。在声明这些库函数的时候,按照其功能和类别,它们被划分到很多不同的头文件中,如:iostream.h,complox.h,stdio.h。当名字空间的概念被提出之后,这些库函数被重新整理,将它们的声明和定义放到了名称空间名称为std的名称空间中。它们被称为标准C++库。
但是为了向前兼容以前实现的C++程序,在对这些库函数进行整理的时候,创建了新的头文件,并采用了新的命名规则,以区分原有的库函数。具体的处理方式描述如下:
对于支持C++的头文件,如:<iostream.h>,在被重新整理之后,它的名称为去掉了头文件的扩展名。新的头文件所包含的功能与旧头文件基本相同,但是它们在std名字空间中; 对于支持C标准的头文件,如:<stdio.h>,在被重新整理之后,它的名称为,在名称的前面加上了前缀字符“C”,并去掉扩展名。新的头文件所包含的功能与旧的头文件基本相同,但是它们在std名字空间中。 原有旧的C++标准头文件,如<iostream.h>,依然被支持,它们不在名字空间std中; 原有旧的C标准头文件,如<stdio.h>,依然被支持,它们不在名字空间std中。
在用户声明的名字空间中还可以继续嵌套其他的名字空间,通过这种分层次的名字空间的结构可以改善函数库的代码组织结构。具体代码如下:
Namespace myFirstSpace
{
Int myVar = 10;
Namespace mySecondSpace
{
int dlVar = 314;
Int myVar = 100;//它会隐藏外围名字空间声明的变量。
}
}
只要需要,名字空间的嵌套可以一直向下持续下去。在名字空间嵌套的时候,外围名字空间声明的变量可能会被里面嵌套的名字空间声明的同名变量隐藏。在使用嵌套名字空间成员的时候,有三种方式,具体情况如下:
//第一种形式:限定修饰名称形式
Int a = MyFirstSpace::mySecondSpace::dlVar;
//第二中形式:using声明的形式:
Using myFirstSpace::mySecondSpace::dlVar;
Int a= dlVar;
//第三中形式:using指示符形式:
Using namespace myFirstSpace::mySecondSpace;
Int a = dlVar;
使用未命名的名字空间,可以定义文件作用域。具有文件作用域的名字空间只在定义它的文件中有效,在其他文件中访问不到该作用域。
未命名名字空间的定义格式如下:
----------------------------A.cpp--------------------------
Namespace
{
Int a = 10;
Void myFunction(int Para)
}
//使用未命名名字空间中的成员
Void main()
{
myFunciton(a);//直接使用,不需要限定修饰。
}
在使用未命名名字空间中的成员的时候,可以直接使用,不需要限定修饰。未命名名字空间中的成员只能在定义它的文件中使用,在其他文件中是无法访问的。
注意点:
目录
C++是一种面向对象的编程语言,它在C语言的基础上发展而来,增加了许多强大的特性,使得开发者能够编写更加高效、模块化和可维护的代码。
C++的一些关键特性:
C++的这些特性使其成为开发高性能应用、系统软件、游戏引擎、实时系统等多种领域应用的优选语言。
章节知识点目录:
C++预备知识函数类与对象数据共享与保护数组、指针与字符串类的继承与派生多态性模板与群体数据STL库流类库与输入输出异常处理重点需要关注的知识点:
struct 与 class 的区别struct 内存对齐问题sizeof与 strlen 区别private、protected、publicstatic, const, extern, volatile 等static_cast、dynamic_cast、const_cast、reinterpret_castauto_ptr、unique_ptr、shared_ptr、weak_ptrstd::move函数STL:vector, list, map, set 等。map 与 unordered_map 对比,set 与 unordered_set 对比,vector 与 list 比较等。本章节知识点目录:
字符集是构成C++语言的基本元素,用C++语言编写编写程序时 ,除字符类型外,其他所有成分都只能由字符集中的字符构成。 C++语言支持以下字符集:
未完待续......
C++的常用关键字包括:auto、decltype、bool、throw、try、catch、class、constexpr、new、delete、const_cast、static_cast、dynamic_cast、reinterpret_cast、explicit、export、friend、mutable、using、namespace、noexcept、nullptr、operator、private、protected、public、static_assert、template、typename、this、thread_local、typeid、virtual等。
我们结合代码示例来介绍各个关键字。
目录
auto自动推断变量decltype推断变量类型bool布尔变量throw/try/catch异常处理class类与对象constexpr常量表达式new与delete管理对象explicit显式调用export全局引用friend友元函数mutable可变变量namespace命名空间noexcept禁止异常nullptr空指针private、protected和publictypeid获取类型信息operator重载操作符template模板this指针thread_local线程私有virtual虚函数auto根据变量初始化来推断变量类型。示例如下:
auto array = new int[8];
与auto不同的是,decltype是根据表达式来推断变量类型。语法格式如下:
decltype(expression) var;
表达式包括:变量、运算、函数等,示例如下:
long add() {
return 0;
}
void hello() {
int a = 2;
decltype(a) b; // b为int类型
decltype(add()) c; // c为long类型
}
bool关键字表示布尔变量,只有true或false两种变量值。
使用关键字throw抛出异常,使用try/catch来捕获异常。示例代码如下:
try {
std::string msg("This is a test exception");
throw msg;
} catch (const std::string &e) {
printf("exception=%s", e.c_str());
}
C++的类与java类相似,都是面向对象编程。类的声明示例如下:
#include <string>
class Person {
private:
int m_age;
std::string m_name;
public:
void setAge(int age);
int getAge();
void setName(const std::string &name);
std::string getName();
};
对应的类实现如下:
#include "Person.h"
void Person::setAge(int age) {
m_age = age;
}
int Person::getAge() {
return m_age;
}
void Person::setName(const std::string &name) {
m_name = name;
}
std::string Person::getName() {
return m_name;
}
创建Person类的对象实例:
auto person = new Person();
person->setAge(10);
person->setName("frank");
printf("name=%s, age=%d", person->getName().c_str(), person->getAge());
constexpr只能修饰带有return的函数。在C++20增加了consteval修饰常量表达式,不同的是,在编译期确定参数类型。示例如下:
constexpr int hello(int a, int b) {
return a + b;
}
另外,函数体内不能有赋值运算,否则有如下报错:
subexpression not valid in a constant expression
在C++提供关键字new来创建对象,delete释放对象。在C语言是用库函数malloc来申请内存,free来释放内存。要注意的是,释放数组需要加上[]。示例如下:
// 创建对象
auto person = new Person();
// 释放对象
delete person;
// 创建数组
auto array = new int[8];
// 释放数组
delete[] array;
C++提供const_cast、static_cast、dynamic_cast和reinterpret_cast四种类型转换,如下所示:
const_cast:用于修改const属性,接受指针或引用类型 static_cast:用于基本类型转换 dynamic_cast:用于有继承关系的类指针转换,支持类型检查 reinterpret_cast:用于指针类型转换类型转换的示例代码如下:
// const_cast用于const属性
const int a = 10;
int &b = const_cast<int &> (a);
// static_cast基本类型转换
float x = 10.5f;
int y = static_cast<int> (x);
// dynamic_cast用于子类与父类转换
SuperMan *superman = new SuperMan();
superman->setAge(100);
Person *person = dynamic_cast<Person*> (superman);
// reinterpret_cast用于指针类型转换
void *data = (void *) "hello";
char *new_data = reinterpret_cast<char *> (data);
explicit用于修饰单参数的构造函数,被修饰的构造函数只能被显式调用,不能被隐式调用。示例如下:
class Person {
private:
int m_age;
public:
explicit Person(int age);
};
C语言有extern关键字用于声明全局变量,但是C++的模板没法用extern修饰。因此,提供export修饰在头文件声明的模板类或模板函数,其他源文件只要引用该头文件即可使用模板类或模板函数。
friend关键字把函数声明为友元函数。声明函数为外部类的友元函数后,外部类可以通过友元函数访问该类的私有成员。示例代码如下:
class Pointer;
class Calculator {
public:
Pointer* add(Pointer &a, Pointer &b);
};
class Pointer {
// 声明为Calculator的友元函数
friend Pointer* Calculator::add(Pointer &a, Pointer &b);
private:
int m_x;
int m_y;
public:
Pointer(int x, int y);
};
// Calculator访问Pointer的私有成员,通过对象访问
Pointer* Calculator::add(Pointer &a, Pointer &b) {
return new Pointer(a.m_x + b.m_x, a.m_y + b.m_y);
}
mutable用于且只能修饰类的成员变量,与const修饰常量相反。比如,constexpr修饰的常量表达式不允许修改成员变量,而成员变量添加mutable修饰符后可修改。示例如下:
class Person {
private:
mutable int m_age;
public:
explicit Person(int age);
constexpr int getAge();
};
constexpr int Person::getAge() {
m_age += 10; // 修改成员变量
return m_age;
}
非成员变量使用mutable修饰符会报错如下:
'mutable' can only be applied to member variable
命名空间用于模块隔离,避免模块之间命名冲突。示例代码如下:
namespace Learning {
class Person {
private:
std::string m_name;
public:
void setName(const std::string &name);
std::string getName();
};
}
使用using引用命名空间,需要注意的是遵循最小原则。示例如下:
// 直接命名空间引用
Learning::Person *person1 = new Learning::Person();
// 引入命名空间
using namespace Learning;
Person *person2 = new Person();
使用noexcept修饰函数禁止抛出异常,防止错误扩散。示例如下:
std::string getName() noexcept;
在C语言使用NULL表示空指针,java使用null表示空指针,Object-C使用nil表示空指针。当然,今天的主角是C++,它使用nullptr表示空指针。
与java类似,C++提供private、protected和public访问修饰符,可以修饰类、函数、变量。三者对比如下:
typeid用于获取类型信息的操作符,使用示例如下:
typeid(a).name()
基本类型对应的类型信息如下:
基本类型/类型信息
在C++中,字符串能够进行加法运算或比较运算,是因为使用operator重载。我们来看下重载加法运算的示例:
class Point {
private:
int m_x;
int m_y;
public:
Point(int x, int y);
int getX();
int getY();
// 重载加法运算
Point operator +(const Point &p) const {
return {m_x + p.m_x, m_y + p.m_y};
}
};
然后调用Point类的加法:
Point p1(1, 2);
Point p2(2, 3);
Point point = p1 + p2;
printf("point.x=%d, point.y=%d\n", point.getX(), point.getY());
template可以用于模板类或模板方法,支持不同数据类型的方法复用。示例如下:
template <typename T>
T add(T a, T b) {
return a + b;
}
然后分别是int类型与float类型的加法运算:
int a = 2, b = 3;
int result1 = add(a, b);
printf("int add=%d\n", result1);
float c = 2.5f, d = 3.5f;
float result2 = add(c, d);
printf("float add=%f\n", result2);
this指针在类内部使用,可以访问类的所有成员。示例如下:
Point::Point(int x, int y) {
this->m_x = x;
this->m_y = y;
}
thread_local用于表示线程私有变量,即每个线程都会存储一个变量的值,线程之间互不共享,是C++提供的存储期关键字。
与thread_local类似的存储期关键字还有:auto、register、static、extern.
各个关键字对比如下:
虚函数是C++的多态机制,使用virtual关键字声明,允许通过基类指针访问基类与派生类的同名函数。基类的析构函数需要声明为虚函数,否则调用不到派生类的析构函数,导致内存泄漏。
示例代码如下:
class Animal {
protected:
std::string m_name;
public:
Animal(const std::string &name);
// 析构函数声明为虚函数
virtual ~Animal();
};
class Cat : public Animal {
public:
Cat(const std::string &name);
~Cat();
};
基类与派生类的实现:
Animal::Animal(const std::string &name) {
m_name = name;
}
Animal::~Animal() noexcept {
printf("Animal release\n");
}
Cat::Cat(const std::string &name) : Animal(name) {
m_name = name;
}
Cat::~Cat() noexcept {
printf("Cat release\n");
}
测试代码,创建Cat类,然后释放:
Animal *cat = new Cat("cat");
delete cat;
打印输出如下,先调用派生类析构函数,再调用基类析构函数:
Cat release
Animal release
标识符是程序员定义的单词,它命名程序正文中的一些实体,如函数名、变量名、类名、对象名等。
C++语言标识符的构成规则:
注意: override,final标识符在特定上下文中有特殊含义,将被用作语法标志而并非普通标识符。类似地C++还有一些标识符保留给标准库,这些应尽量避免使用。
操作符是用于实现各种运算的符号,例如:+、——、、/、%、=、==、!=、<、>、<=、>=、++、--、!、&&、||、&、|、^、~、<<、>>、+=、-=、=、/=、%=、&=、|=、^=、<<=、>>=、->、->*、[]、()、new、delete、new[]、delete[]、typeid、dynamic_cast、static_cast、reinterpret_cast、const_cast、sizeof、alignof、noexcept、
C++中,还提供了一些操作符的替代名:
and,andeq,bitand,bitor,compl,not,not_eq,or,or_eq,xor,xor_eq
分隔符用于分隔各个词法记号或程序正文,C++语言中的分隔符是:
; : , {} ()
在程序编译时的词法分析阶段将程序正文分解为词法记号,空白符将被忽略。
空白符:空格、制表符、换行符、回车符、换页符、垂直制表符。
C++中基本数据类型:
int型(也包括short和long)在默认(不加修饰)情况下是有符号的(signed)的。
常量是指在程序运行整个过程中其值始终不改变的量,也就是直接使用符号(文字)表示的值。例如:
12,3.5,A都是常量
int,long,long long钟能容纳其数值的尺寸中最小的一个,八进制和十六进制整型常量的类型则是能容纳其数值的int,unsigned int,unsigned long,long long,unsigned long long钟能容纳其数值的尺寸中最小的一个。long,后缀LL(或ll)表示类型是long long,后缀U(或u)表示unsigned类型。实型常量即以文字形式出现的实数,实数有两种表示形式:
注意:实型常量默认为double型,如果后缀"F"(或“f”)可以使其成为float型,例如“12.3f”。
字符常量是单引号括起来的一个字符,如:'a','A','$','?'等。
另外还有一些无法通过键盘输入,例如响铃,换行,制表符,回车等。这些字符常量该如何写入程序中呢?
使用转义字符或者ASCLL码来实现。
例如:
'a'的十六进制ASCLL码为61,于是可以用\x61来表示。
注意:字符数据在内存中以ASCLL码的形式存储,每个字符占用一个字节,使用7个二进制位。
字符串常量简称字符串,是用一对双引号括起来的字符序列,如:"Hello World!"。都是字符串常量。
u,unicode 16 字符,char16_tU,unicode 32 字符,char32_tL,宽字符,wchar_tu8,UTF-8字符(),char注意:"a"和'a'的区别是:"a"是字符串常量,'a'是字符常量。"a":a,\0'a':a
布尔型常量只有两个:false(假)和true(真)。
在程序的执行过程中其值可以发生改变的量称为变量,变量是需要名字来标识的。
就像常量具有各种类型一样,变量也具有相应的类型,变量在使用前需要首先申明其类型和名称。
未完待续。。。
在定义一个变量的同时,也可以为它设置初始值,称为对变量的初始化,例如:
int a=3;
double f=3.14;
char c='a';
知识点:
double pi=3.14,c=3*pi;
int a=0;
int a(0);
int a{0};
int a={0};
double pi=3.14;
int a{pi},b={pi};//错误,转换未执行,存在丢失信息的风险
int c(pi),d=pi;//正确,转换执行,不存在丢失信息的风险
变量除了具有数据类型外,还有具有存储类型。
变量的存储类型决定了其存储方式:
除了前面讲过的直接用文字表示常量外,也可以为常量命名,这就是符号常量。
符号常量在使用之前一定要首先声明
const 数据类型说明符 常量名 = 常量值;
或
数据类型说明符 const 常量名 = 常量值;
例:
const float PI=3.1415926;
注意:符号常量在声明时一定要初始化,而在程序中间不能改变其值。,例如下列语句是错误的:
const float PI;//错误,常量在声明时必须被初始化
PI=3.1415926;//错误,常量不能改变其值
意义:
常量表达式是一类值不能发生改变的表达式,其值在编译期就确定。
const int_max_size=100;
const int limit=int_max_size+1;
int student_size=30;//student_size
const int size=get_size();//不是常量表达式
在实际编程中会用constexpr来表示常量表达式。
未完待续
+ 、- 、* 、/ 、%<< >>& | ^ = 、+= 、 -= 、 *= 、 /= 、%= 、<<= 、>>= 、&= 、|= 、^=!、++、--、&、*、+、-、~ 、sizeof、(类型) > 、>= 、< 、<= 、 == 、 !=&& 、||? :,[](). 、->有+、-、*、/、%这些
其中/是除运算,%是取模(取余数)运算.
这些操作符都是双目操作符。 他们都是有2个操作数的,位于操作符两端的就是它们的操作数,这种操作符也叫双目操作符。
演示
int main()
{
int a = 7 / 2;
//如果是float a =7/2,结果为3.0,原因就在于 C 语⾔⾥⾯的整数除法是整除,只会返回整数部分,丢弃⼩数部分。
printf("%d\n",a);
float b =7 / 2.0;
printf("%f\n",b);//除号两端都是整数的时候,执行的是整数除法,如果两端只要有一个浮点数就执行浮点数的除法.
printf("%.1f\n",b);//结果保留一位小数,`.2`则表示保留两位,以此类推.
}
再看⼀个例子:
#include <stdio.h>
int main()
{
int score = 5;
score = (score / 20) * 100;
return 0;
}
//结果为0
上⾯的代码,你可能觉得经过运算, score 会等于 25 ,但是实际上 score 等于 0 。这是因为 score / 20 是整除,会得到⼀个整数值 0 ,所以乘以 100 后得到的也是 0 。 为了得到预想的结果,可以将除数 20 改成 20.0 ,让整除变成浮点数除法。
#include <stdio.h>
int main()
{
int score = 5;
score = (score / 20.0) * 100;
return 0;
}
//结果为0
运算符 % 表⽰求模运算,即返回两个整数相除的余值。这个运算符只能⽤于整数,不能⽤于浮点数。
int c = 7 % 2;
printf("%d\n",c);//取模是不能写浮点数的
负数求模的规则是,结果的正负号由第⼀个运算数的正负号决定。
#include <stdio.h>
int main()
{
printf("%d\n", 11 % -5); // 1
printf("%d\n",-11 % -5); // -1
printf("%d\n",-11 % 5); // -1
return 0;
}
上⾯⽰例中,第⼀个运算数的正负号( 11 或 -11 )决定了结果的正负号。
<< 左移操作符>>右移操作符>> << (涉及二进制)
注:移位操作符的操作数只能是整数。
移位规则:左边抛弃、右边补0
#include <stdio.h>
int main()
{
int num = 10;
int n = num<<1;//num的值是不变的
printf("n= %d\n", n);
printf("num= %d\n", num);
return 0;
}
移位规则:首先右移运算分两种:
1.逻辑右移:左边用0填充,右边丢弃
2.算数右移:左边用原该值的符号位填充,右边丢弃
注意:对于移位运算符,不要移动负数位,这个是标准未定义的。
& ^ | ~
按位与
按位或
按位异或
按位取反
注:他们的操作数必须是整数。
#include <stdio.h>
int main()
{
int num1 = -3;
int num2 = 5;
printf("%d\n", num1 & num2);
printf("%d\n", num1 | num2);
printf("%d\n", num1 ^ num2);
printf("%d\n", ~0);
return 0;
}
⼀道变态的⾯试题: 不能创建临时变量(第三个变量),实现两个整数的交换。
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a^b;
b = a^b;
a = a^b;
printf("a = %d b = %d\n", a, b);
return 0;
}
练习1:编写代码实现:求⼀个整数存储在内存中的⼆进制中1的个数。
//⽅法1
#include <stdio.h>
int main()
{
int num = 10;
int count= 0;//计数
while(num)
{
if(num%2 == 1)
count++;
num = num/2;
}
printf("⼆进制中1的个数 = %d\n", count);
return 0;
}
//⽅法2:
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
for(i=0; i<32; i++)
{
if( num & (1 << i) )
count++;
}
printf("⼆进制中1的个数 = %d\n",count);
return 0;
}
//⽅法3:
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
while(num)
{
count++;
num = num&(num-1);
}
printf("⼆进制中1的个数 = %d\n",count);
return 0;
}
//这种⽅式是不是很好?达到了优化的效果,但是难以想到。
练习2:⼆进制位置0或者置1
编写代码将13⼆进制序列的第5位修改为1,然后再改回0
13的2进制序列: 00000000000000000000000000001101
将第5位置为1后:00000000000000000000000000011101
将第5位再置为0:00000000000000000000000000001101
参考代码:
#include <stdio.h>
int main()
{
int a = 13;
a = a | (1<<4);
printf("a = %d\n", a);
a = a & ~(1<<4);
printf("a = %d\n", a);
return 0;
}
在变量创建的时候给⼀个初始值叫初始化,在变量创建好后,再给⼀个值,这叫赋值。
int a = 100;//初始化
a = 200;//赋值,这⾥使⽤的就是赋值操作符
赋值操作符=是⼀个随时可以给变量赋值的操作符。
赋值操作符也可以连续赋值,如:
int a = 3;
int b = 5;
int c = 0;
c = b = a+3;////连续赋值,从右向左依次赋值的。
C语⾔虽然⽀持这种连续赋值,但是写出的代码不容易理解,建议还是拆开来写,这样⽅便观察代码的执⾏细节。
在写代码时,我们经常可能对⼀个数进⾏⾃增、⾃减的操作,如下代码:
int a = 10;
a += 3;
a -= 2;
复合操作符还有
= += -= *= /= &=
^= |= >>= <<=
前⾯介绍的操作符都是双⽬操作符,有2个操作数的。C语⾔中还有⼀些操作符只有⼀个操作数,被称为单目操作符。 ++、--、+(正)、-(负) 就是单⽬操作符的。
++是⼀种⾃增的操作符,⼜分为前置++和后置++,--是⼀种⾃减的操作符,也分为前置--和后置--.
前置++
int a = 10;
int b = ++a;//++的操作数是a,是放在a的前⾯的,就是前置++
printf("a=%d b=%d\n",a , b);
计算⼝诀:先+1,后使⽤; a原来是10,先+1,后a变成了11,再使⽤就是赋值给b,b得到的也是11,所以计算技术后,a和b都是11,相当于这样的代码:
int a = 10;
a = a+1;
b = a;
printf("a=%d b=%d\n",a , b);
后置++
int a = 10;
int b = a++;//++的操作数是a,是放在a的后⾯的,就是后置++
printf("a=%d b=%d\n",a , b);
计算⼝诀:先使⽤,后+1 a原来是10,先使⽤,就是先赋值给b,b得到了10,然后再+1,然后a变成了11,所以直接结束后a是11,b是10,相当于这样的代码:
int a = 10;
int b = a;
a = a+1;
printf("a=%d b=%d\n",a , b);
同理--前置、后置
- 负值
用来改变数字前的符号.
+ 正值
实际上没什么意义.
& 取地址(和指针有关系)
*解引用操作符
sizeof是单目操作符,求操作数的类型长度(以字节为单位)
.在结构体struct中,.为结构成员访问操作符
->在结构体struct中,->用于连接结构体指针变量和成员名称
int main()
{
int arr[10] = { 0 };//初始化数组
printf("%d\n",sizeof(arr));
//结果为40,计算的是整个数组的大小,单位是字节
printf("%d\n",sizeof(arr[10]));
//计算的结果为4,是数组中的一个元素的大小,单位是字节
printf("%d\n",sizeof(arr)/sizeof(arr[0]));输出的结果为10,是数组元素的个数
}
~
对一个数的二进制按位取反(后面学)
*间接访问操作符(解引用操作符)(后续会解释)
(类型)强制类型转换,括号内的类型是想要转换的类型
在c语言中,对于像3.14这样的的字面浮点数,编译器默认理解为double类型
int a = (int)3.14
printf("%d\n",a);
C 语⾔⽤于⽐较的表达式,称为 “关系表达式”(relational expression),⾥⾯使⽤的运算符就称为“关系运算符”(relational operator),主要有下⾯6个。
>大于操作符>=⼤于等于运算符 <⼩于运算符 <=⼩于等于运算符 !=不相等运算符 ==相等运算符关系表达式通常返回 0 或 1 ,表⽰真假。
C 语⾔中, 0 表⽰假,所有⾮零值表⽰真。⽐如, 20 > 12 返回 1 , 12 > 20 返回 0 。
关系表达式常⽤于 if 或 while 结构。
if (x == 3)
{
printf("x is 3.\n");
}
另⼀个需要避免的错误是:多个关系运算符不宜连⽤。
i < j < k
上⾯⽰例中,连续使⽤两个⼩于运算符。这是合法表达式,不会报错,但是通常达不到想要的结果,即不是保证变量j的值在i和 k 之间。因为关系运算符是从左到右计算,所以实际执⾏的是下⾯的表达式。
(i < j) < k
上⾯式⼦中, i < j 返回 0 或 1 ,所以最终是 0 或 1 与变量 k 进⾏⽐较。如果想要判断变量j的值是否在 i 和 k 之间,应该使⽤下⾯的写法。
i < j && j < k
&& 就是与运算符,也是并且的意思, && 是⼀个双⽬操作符,使⽤的⽅式是 a&&b , && 两边的表达式都是真的时候,整个表达式才为真,只要有⼀个是假,则整个表达式为假。
|| 就是或运算符,也就是或者的意思, || 也是⼀个双⽬操作符,使⽤的⽅式是 a || b , ||两边的表达式只要有⼀个是真,整个表达式就是真,两边的表达式都为假的时候,才为假。
int main()
{
int a = 10;
int b = 4;
if (a && b)//(a || b)
{
printf("hehe\n");
}
return 0;
}
! 逻辑反操作c语言中,0表示假
非0表示真,!的作用就是把假的变成真的,把真的变成假的
int main()
{
int flag = 0;//0表示假,非零表示真
if (!flag)//if语句中默认条件为真时执行
{
printf("haha\n");
}
}
练习 闰年的判断 输⼊⼀个年份year,判断year是否是闰年 闰年判断的规则:
#include <stdio.h>
//代码1
int main()
{
int year = 0;
scanf("%d", &year);
if(year%4==0 && year%100!=0)
printf("是闰年\n");
else if(year%400==0)
printf("是闰年\n");
return 0;
}
//代码2
int main()
{
int year = 0;
scanf("%d", &year);
if((year%4==0 && year%100!=0) ||(year%400==0))
printf("是闰年\n");
return 0;
}
C语⾔逻辑运算符还有⼀个特点,它总是先对左侧的表达式求值,再对右边的表达式求值,这个顺序是保证的。如果左边的表达式满⾜逻辑运算符的条件,就不再对右边的表达式求值。这种情况称为“短路”。 如前⾯的代码:
if(month >= 3 && month <= 5)
表达式中&& 的左操作数是 month >= 3 ,右操作数是 month <= 5 ,当左操作数 month >= 3 的结果是0的时候,即使不判断 month <= 5 ,整个表达式的结果也是0(不是春季)。 所以,对于&&操作符来说,左边操作数的结果是0的时候,右边操作数就不再执⾏。
对于 || 操作符是怎么样呢?我们结合前⾯的代码:
if(month == 12 || month==1 || month == 2)
如果month == 12,则不⽤再判断month是否等于1或者2,整个表达式的结果也是1(是冬季)。所以, || 操作符的左操作数的结果不为0时,就⽆需执⾏右操作数。像这种仅仅根据左操作数的结果就能知道整个表达式的结果,不再对右操作数进⾏计算的运算称为短路求值。
#include <stdio.h>
int main()
{
int i = 0,a=0,b=2,c =3,d=4;
i = a++ && ++b && d++;
//i = a++||++b||d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
两种情况结果分别是
a = 1
b = 2
c = 3
d = 4
和
a = 1
b = 3
c = 3
d = 4
三目操作符,有三个操作数exp1 ? exp2 : exp3
注意符号依次是:问号,冒号,分号
条件操作符的计算逻辑是:如果 exp1 为真, exp2 计算,计算的结果是整个表达式的结果;如果exp1 为假, exp3 计算,计算的结果是整个表达式的结果。
例子
int a = 10;
int b = 20;
int c = (a > b ? a : b);
逗号表达式,就是⽤逗号隔开的多个表达式。
逗号表达式,从左向右依次执⾏。整个表达式的结果是最后⼀个表达式的结果。
下面代码的结果是:
#include <stdio.h>
int main()
{
int a, b, c;
a = 5;
c = ++a;
b = ++c, c++, ++a, a++;
b += a++ + c;
printf("a = %d b = %d c = %d\n:", a, b, c);
return 0;
}
结果是 a = 9 b= 23 c = 8
//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
c是多少?
//代码2
if (a =b + 1, c=a / 2, d > 0)
//代码3
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
//...
a = get_val();
count_val(a);
}
如果使⽤逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
[]arr[3]中的[]就是下标引用操作符arr和3就是[]的操作数
操作数:⼀个数组名 + ⼀个索引值(下标)
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
//创建数组的时候,[]中不能是变量
int n = 3;
arr[n] = 20;//访问元素时,[]中可以是变量
()接受⼀个或者多个操作数:第⼀个操作数是函数名,剩余的操作数就是传递给函数的参数。
int Add(int x,int y)
{
return x+y;
}
int main()
{
int sum =Add(2,3);//()就是函数调用操作符
//Add,2,3都是()的操作数
return 0;
}
C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等;描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类型,让程序员可以⾃⼰创造适合的类型。
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚⾄是其他结构体。
struct tag
{
member-list;
}variable-list;
描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
//代码1:变量的定义
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//代码2:初始化。
struct Point p3 = {10, 20};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s1 = {"zhangsan", 20};//初始化
struct Stu s2 = {.age=20, .name="lisi"};//指定顺序初始化
//代码3
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下所⽰:
#include <stdio.h>
struct Point
{
int x;
int y;
}p = {1,2};
int main()
{
printf("x: %d y: %d\n", p.x, p.y);
return 0;
}
使⽤⽅式:结构体变量.成员名
有时候我们得到的不是⼀个结构体变量,⽽是得到了⼀个指向结构体的指针。如下所⽰:
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3, 4};
struct Point *ptr = &p;
ptr->x = 10;
ptr->y = 20;
printf("x = %d y = %d\n", ptr->x, ptr->y);
return 0;
}
使⽤⽅式:结构体指针->成员名
#include <stdio.h>
#include <string.h>
struct Stu
{
char name[15];//名字
int age; //年龄
};
void print_stu(struct Stu s)
{
printf("%s %d\n", s.name, s.age);
}
void set_stu(struct Stu* ps)
{
strcpy(ps->name, "李四");
ps->age = 28;
}
int main()
{
struct Stu s = { "张三", 20 };
print_stu(s);
set_stu(&s);
print_stu(s);
return 0;
}
引用不是新定义一个变量,而是给已存在的变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型 & 引用变量名 (对象名)(另取的别名) = 引用实体
注意: 引用类型必须和引用实体是同种类型的!
void TestRef()
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra);
}
加了 const 该变量是不能修改的,即成了一种常量,可以理解为只读型,而没加 const 的可理解为可读可写型。
1.权限的平移 --- 两者都是可读可写型
int a = 10;
int & b = a;
2.权限放大:
//只读型(const)
const int a = 20;
//可读可写型 报错!
int &b= a; × × ×
//权限平移
const int &c = a; √ √ √
3.权限缩小:
//可读可写型
int e = 30;
//只读型
const int &f = e; √ √ √
! 临时变量具有常属性 !
类型转换,并不会改变变量类型,中间都会产生一个临时变量!!
//临时变量具有常属性 --- 相当于被const修饰了
int ii = 1;
//发生 -- 隐式转换 --
double dd = ii; //隐式类型转换是语法允许的
--------------------------
//报错 权限被放大
double &rdd = ii; // × × × 权限被放大了
//权限的平移
const double &rdd = ii;
参数是调用函数与被调用函数之间交换的通道,函数定义的首部的参数称为形式参数(简称形参),调用函数时使用的参数称为实际参数(简称实参)。
简介:在值传递机制中,作为实参的表达式的值被复制到由对应的形参名所标识的对象中,成为形参的初值。完成参数值传递之后,函数体中的语句对形参的访问、修改都是在这个标识对象上操作的,与实参对象无关。
1)第一种格式,是在一开始就声明函数体,并赋予形参整形变量x,y,并在函数体里面对形参进行所需的运算,最后才写主函数,主函数中设置好实参a,b的值,再调用函数count将实参传回去计算。
#include<iostream>
using namespace std;
void count(int x,int y)
{
x=x*2;
y=y*y;
cout<<"x="<<x<<'\t';
cout<<"y="<<y<<endl;
}
int main()
{
int a=3,b=4;
count(a,b);
cout<<"a="<<a<<'\t';
cout<<"y="<<b<<endl;
}
2)第二种格式,是先写主函数,再写函数体。注意:这个时候会出现报错。为什么会报错?因为主函数找不到你所调用的函数在哪里。解决办法是:在主函数之前声明该函数即可。
#include<iostream>
using namespace std;
void count(int x,int y);//函数声明
int main()
{
int a=3,b=4;
count(a,b);
cout<<"a="<<a<<'\t';
cout<<"y="<<b<<endl;
}
void count(int x,int y)
{
x=x*2;
y=y*y;
cout<<"x="<<x<<'\t';
cout<<"y="<<y<<endl;
}
简介:当函数定义中的形参被说明为指针类型时,称为指针参数。形参指针对应的实参是地址表达式。调用函数时,实参把对象的地址赋给形参名标识的指针变量,被调用的函数可以在函数体内通过形参指针来间接访问实参地址所指的对象。这种参数传递方式称为指针传递或地址调用。
1)下面的例子中,x和y分别获取了a和b的地址,然后再通过swap函数进行交换。
#include<iostream>
using namespace std;
void swap(int *x,int *y)
{
int temp=*x;
*x=*y;
*y=temp;
}
int main()
{
int a=3,b=8;
cout<<"before swaping.....\n";
cout<<"a="<<a<<",b="<<b<<endl;
swap(&a,&b);
cout<<"after swaping.....\n";
cout<<"a="<<a<<",b="<<b<<endl;
}
2)如果我们不要指针那还会不会实现上面的交换功能呢?
#include<iostream>
using namespace std;
void swap(int x,int y)
{
int temp;
temp=x;
x=y;
y=temp;
}
int main()
{
int a=3,b=8;
cout<<"before swaping.....\n";
cout<<"a="<<a<<",b="<<b<<endl;
swap(a,b);
cout<<"after swaping.....\n";
cout<<"a="<<a<<",b="<<b<<endl;
}
无法实现功能
简介:形参被定义为引用类型时被称为引用参数。引用参数对应的实参应该是对象名。函数被调用时,形参不需要开辟新的储存空间,形参名作为引用(别名)绑定于实参标识的对象上,执行函数体时,对形参的操作就是对实参对象的操作,直至函数执行结束,撤销引用绑定。
#include<iostream>
using namespace std;
void swap(int &,int &);
int main()
{
int a=3,b=8;
cout<<"before swaping.....\n";
cout<<"a="<<a<<",b="<<b<<endl;
swap(a,b);
cout<<"after swaping.....\n";
cout<<"a="<<a<<",b="<<b<<endl;
}
void swap(int &x,int &y)
{
int temp=x;
x=y;
y=temp;
}
这里类似像取别名了,就是 int &x=a; int &y=b。
#include<iostream>
using namespace std;
void display(const int& rk) //定义const引用参数
{
cout<<rk<<":\n"<<"dec:"<<rk<<endl<<"oct:"<<oct<<rk<<endl
<<"hex:"<<hex<<rk<<endl;
}
int main()
{
int m=2618;
display(m); //实参是变量
display(4589); //实参是常数
}
在本例main函数中第二次调用display函数时,用常数4589作为实参。C++规定,**函数的const引用参数允许对应的实参为常数或者表达式。**调用函数进行参数传递时将产生一个匿名对象保存实参的值。形参标识名作为这个匿名对象的引用,对匿名对象进行操作。匿名对象在被调用函数运行结束后撤销。
const引用参数的匿名对象测试
#include<iostream>
using namespace std;
void anonym (const int &ref)
{
cout<<"The address of val is:"<<&ref<<endl;
}
int main()
{
int val=10;
cout<<"The address of val is:"<<&val<<endl;
anonym(val);
anonym(val+5);
}
main函数第一次调用anonym函数时,实参是变量名。形参ref与实参val绑定。在程序输出的第1行和第2行,实参和形参的地址相同,说明引用参数与实参对象都是同一个储存单元,引用参数以别名方式在实参对象上进行操作。无论形参是否被约束,情形都一样。第2次调用anonym函数时,实参是表达式。(注意:C++为const引用建立匿名对象用于存放val+5的值。第3行输出是匿名对象的地址。只有const引用对应的实参可以是常量或表达式,非约束的引用参数对应的实参必须是对象名。)
函数是一个可以重复使用的代码块,CPU 会一条一条地挨着执行其中的代码。CPU 在执行主调函数代码时如果遇到了被调函数,主调函数就会暂停,CPU 转而执行被调函数的代码;被调函数执行完毕后再返回到主调函数,主调函数根据刚才的状态继续往下执行。
一个 C/C++程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条,这个链条的起点是 main(),终点也是 main()。当 main() 调用完了所有的函数,它会返回一个值(例如return 0;)来结束自己的生命,从而结束整个程序。
**函数调用是有时间和空间开销的。**程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。
如果函数体代码比较多,需要较长的执行时间,那么函数调用机制占用的时间可以忽略;如果函数只有一两条语句,那么大部分的时间都会花费在函数调用机制上,这种时间开销就就不容忽视。
为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function),又称内嵌函数或者内置函数。
#include <iostream>
using namespace std;
//内联函数,交换两个数的值
inline void swap(int *a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
int main()
{
int m, n;
cin>>m>>n;
cout<<m<<", "<<n<<endl;
swap(&m, &n);
cout<<m<<", "<<n<<endl;
return 0;
}
//运行结果:
//45 99
//45, 99
//99, 45
注意,要在函数定义处添加 inline 关键字,在函数声明处添加 inline 关键字虽然没有错,但这种做法是无效的,编译器会忽略函数声明处的 inline 关键字。
当编译器遇到函数调用swap(&m, &n)时,会用 swap() 函数的代码替换swap(&m, &n),同时用实参代替形参,以下为替换后的结果:
int temp;
temp = *(&m);
*(&m) = *(&n);
*(&n) = temp;
编译器可能会将 (&m)、(&n) 分别优化为 m、n。
当函数比较复杂时,函数调用的时空开销可以忽略,大部分的 CPU 时间都会花费在执行函数体代码上,所以我们一般是将非常短小的函数声明为内联函数。
由于内联函数比较短小,我们通常的做法是省略函数原型,将整个函数定义(包括函数头和函数体)放在本应该提供函数原型的地方。下面的例子写法是不被推荐的:
#include <iostream>
using namespace std;
//声明内联函数
void swap1(int *a, int *b); //也可以添加inline,但编译器会忽略
int main()
{
int m, n;
cin>>m>>n;
cout<<m<<", "<<n<<endl;
swap1(&m, &n);
cout<<m<<", "<<n<<endl;
return 0;
}
//定义内联函数
inline void swap1(int *a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
使用内联函数的缺点也是非常明显的,编译后的程序会存在多份相同的函数拷贝,如果被声明为内联函数的函数体非常大,那么编译后的程序体积也将会变得很大,所以再次强调,一般只将那些短小的、频繁调用的函数声明为内联函数。
对函数作 inline 声明只是程序员对编译器提出的一个建议,而不是强制性的,并非一经指定为 inline 编译器就必须这样做。编译器有自己的判断能力,它会根据具体情况决定是否这样做。
使用宏代码最大的缺点是容易出错,预处理在拷贝宏代码时常常产生意向不到的边际效应。 例如:
#define MAX(a,b) (a)>(b)?(a):(b)
语句:
result = MAX(i,j)+2;
//被预处理器扩展为
result = (i)>(j)?(i):(j)+2;
由于运算符"+"比运算符"?:"的优先级高,所以上述语句并不等价于
result = ((i)>(j)?(i):(j))+2;
如果把宏代码改写为:
#define MAX(a,b) ((a)>(b)?(a):(b))
则能解决优先级的问题。但是会引发另一个问题
result = MAX(i++,j);
//被预处理器扩展为
result = (i++)>(j)?(i++):(j) //在同一个表达式中i被两次求值。
宏的另一个缺点是不可调试。
内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作的类的数据成员,所以应该尽量使用内联函数来取代宏代码。
内联能提高函数的执行效率,那么为什么不把所有的函数都定义成内联函数呢? 内联不是万灵丹,它以代码膨胀(拷贝)为代价,仅仅省区了函数调用的开销,从而提高程序的执行效率。(开销指的是参数的压栈、跳转、退栈和返回操作)。
一方面,如果执行函数体内代码的时间比函数调用的开销大得多,那么inline效率收益会很小。另一方面,每一处内联函数的调用都要拷贝代码,使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:
函数在定义时可以预先给出默认的形参值。
调用时:
int add(int x=5,int y=6)
{
return x+y;
}
void main(void)
{
add(10,20); //10+20
add(10); //10+6
add(); ////5+6
}
默认形参值必须从右向左顺序声明,并且在默认形参值的右面不能有非默认形参值的参数。
int add(int x,int y=5,int z=6);
int add(int x=1,int y=5, int z); //错误的
int add(int x=1, int y, int z=6); //错误的
在相同的作用域内,默认形参值的说明应保持唯一,但如果在不同的作用域内,允许说明不同的默认形参。
int add(int x=1,int y=2);
int main()
{
int add(int x=3,int y=4);
add(); //使用局部默认形参值(实现3+4)
return 0;
}
void fun(void)
{ ...
add();//使用全局默认形参值(实现1+2)
}
A (A& a); //拷贝构造函数
A (const A& a); //拷贝构造函数
A& operator= (const A& a); //赋值构造函数
一个类的对象被创建的时候,编译系统为对象分配内存空间,并自动调用该构造函数,由构造函数完成成员的初始化工作。因此,构造函数的核心作用就是,初始化对象的数据成员。
#include <iostream>
using namespace std;
//声明Time类
class Time
{
public: //成员函数共有部分
Time() //定义构造成员函数,函数名与类名相同
{
hour= 0;//利用构造函数给对象中的数据成员都赋初值为0
minute= 0;
sec= 0;
}
//成员函数的声明
void set_time();
void show_time(void);
private: //类的私有数据部分
int hour; //默认数据也是私有的
int minute;
int sec;
};
//定义成员函数
//获取时间数据函数
void Time::set_time(void)
{
cin >> hour;
cin >> minute;
cin >> sec;
}
//显示时间格式的函数
void Time::show_time(void) //显示时间函数
{
cout << hour << ":" << minute << ":" << sec << endl;
}
//主函数
int main()
{
Time t1; //通过类Time实例化对象t1
t1.set_time(); //调用成员函数,给t1的数据成员赋值
t1.show_time(); //显示t1的数据成员值
return 0;
}
简单来说,就是构造函数定义中带有参数,然后对参数进行操作;
调用构造函数内==实例化对象;
在调用时,传入参数。和无参数的构造函数的调用做个对比就很清晰了:
//假设已经定义了一个类Box,则在实例化对象时,也就是调用构造函数时:
Box b1; //实例化无参构造函数
Box b2(10001, "Chung", 'F'); //实例化带参构造涵数,传入指定的参数
//提示:正常情况下没有定义构造函数的话,就会默认生成一个构造函数,不影响数据的赋值,也不需要调用
#include <iostream>
using namespace std;
class Box
{
public:
Box(int, int, int); //声明带参数的构造函数
int Volume(); //声明计算体积的构造函数
private: //私有数据部分定义长宽高的变量
int height;
int width;
int length;
};
//定义Box类的构造函数 ,带有3个参数
Box::Box(int h, int w, int len)
{
height= h; //对私有成员进行初始化
width= w;
length= len;
}
//也可以简化写成一行:Box(int h, int w, int len):height(h), width(w), length(len){ }
//定义计算体积的成员函数
Box::Volume()
{
return(height * width * length); //计算体积
}
//定义主函数
int main()
{
//由于构造函数是带有参数的,因此实例化时需要传入参数
Box b1(12, 25, 30); //通过Box类实例化对象b1
cout << "盒子1的体积为:" << b1.Volume() << endl;
Box b2(15, 30, 21); //实例化对象b2
cout << "盒子2的体积为:" << b2.Volume() << endl;
return 0;
}
在定义带参构造函数时,可以两种写法,都可以进行传参。 (1)正常写法
//加入已经定义了类Box,则构造函数的定义如下:
Box(int h, int w, int len)
{
height= h; //对私有成员进行初始化
width= w;
length= len;
}
(2)初始化列表写法
Box(int h, int w, int len):height(h), width(w), length(len){ }
这种写法是需要直接定义参数变量再对成员变量赋值的,而是写成了一行,
注意点:变量必须一一对应才能正常传参。
就是带有默认参数的构造函数,在实例化时若传入参数,则传入的参数值优先;若没有传入参数,则就使用指定的默认参数。
#include <iostream>
using namespace std;
class Box
{
public:
Box(int h=10, int w=10, int len=10); //声明带参数的构造函数
int Volume(); //声明计算体积的构造函数
private: //私有数据部分定义长宽高的变量
int height;
int width;
int length;
};
//定义Box类的构造函数 ,带有3个参数
Box::Box(int h, int w, int len)
{
height= h; //对私有成员进行初始化
width= w;
length= len;
}
//也可以不指定默认参数: Box::Box(int h, int w, int len){ }
//定义计算体积的成员wa函数
Box::Volume()
{
return(height * width * length); //计算体积
}
//定义主函数
int main()
{
//由于构造函数是带有默认参数的,因此实例化时可以不传入参数
Box b1; //通过Box类实例化对象b1
cout << "盒子1的体积为:" << b1.Volume() << endl;
//传入不同个数参数的对象
Box b2(1); //实例化对象b2,传入一个参数 ,默认对应第一个参数,即int h=1
cout << "盒子2的体积为:" << b2.Volume() << endl;
Box b3(1, 1); //通过Box类实例化对象b3 ,h=1, w=1
cout << "盒子3的体积为:" << b3.Volume() << endl;
Box b4(1, 1, 1); //实例化对象b4, h=1, w=1, len=1
cout << "盒子4的体积为:" << b4.Volume() << endl;
return 0;
}
构造函数的使用情况:
#include <iostream>
using namespace std;
class Test
{
public:
// 构造函数
Test(int a):t_a(a){cout<<"creat: "<<t_a<<endl;}
// 拷贝构造函数
Test(const Test& T)
{
t_a = T.t_a;
cout<<"copy"<<endl;
}
// 析构函数
~Test()
{
cout<<"delete: "<<t_a<<endl;
}
// 显示函数
void show()
{
cout<<t_a<<endl;
}
private:
int t_a;
};
// 全局函数,传入的是对象
void fun(Test C)
{
cout<<"test"<<endl;
}
int main()
{
Test t(1);
// 函数中传入对象
fun(t);
return 0;
}
如果自定义了拷贝构造函数,则系统不会默认生成拷贝构造函数了。自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。
//假如已经定义了一个类Box,则通过以下方式定义拷贝构造函数:
Box(const Box &p)
{
age= p.age;
name= p.name;
}
//调用拷贝构造函数
Box b3(b2); //传入参数就是一个对象b2
在定义拷贝函数那个括号中:p是一个引用类型,括号内相当于Box p=b2,b2是已经实例化的一个对象,const加上&就是常量引用了。
因此,**拷贝构造就是简单的拷贝值,因为它就是个常量引用,**此处就是引用了对象b2。调用那句,就是说明通过Box类实例化一个对象b3,引用了对象b2的数据。
简单来说,就是没有名字的对象,这个对象只能用一次,只在定义行起作用,一般情况是不会去用它的。
//假如已经定义好了类Box,则可以有以下三种匿名对象的实例化:
Box (10, "Chung"); //有参构造函数匿名对象
Box (); //无参构造函数匿名对象
//有名对象调用(用以区别匿名对象)
Box p(15, "Hawk");
注意:匿名对象不能用括号法调用拷贝函数,也就是说不能写成这样:Box (b2);
//假如已经定义好了类Box,则实例化对象时可以用显示法:
Box b1= Box(10, "Chung"); //调用有参构造函数
Box b2= Box(); //调用无参构造函数
Box b3= Box(b1); //调用拷贝函数
//假如已经定义好了类Box,则调用使用隐式法实例化对象为以下三种情况:
Box b1= {10, "Chung"}; //调用有参构造函数
Box b2= b1; //调用拷贝函数
//注意:隐式法无法调用无参构造函数,也就是不能写成:
Box b3={};
禁止隐式法调用构造函数可在构造函数定义前加上:explicit
与普通的函数重载基本是没有区别,就是同一个函数名因为参数不同代表不同的函数,只是这里的构造函数都没有返回值:
#include <iostream>
using namespace std;
//声明一个Box类
class Box
{
public:
Box(); //声明一个无参数的构造函数(并未定义)
//定义一个有参数的构造函数,用参数的初始化表对数据成员初始化
Box(int h, int w, int len):height(h), width(w), length(len){ }
//相当于:
/*Box(int h, int w, int len)
{
h= height;
w= width;
length= len;
} */
int Volume(); //声明成员函数V,也就是计算体积的函数
private: //私有部分,数据成员的定义
int height;
int width;
int length;
};
//在类外面定义无参数的构造函数Box
Box::Box()
{
height= 10; //在构造函数里对类的私有成员进行私有化
width= 10;
length= 10;
}
//在类外定义进行有长、宽、高计算的成员函数
int Box::Box::Volume()
{
return (height * width * length);
}
int main()
{
Box b1; //通过Box类实例化对象b1
cout << "通过无参构造函数初始化的盒子体积为:" << b1.Volume() << endl;
Box b2; //实例化对象b2
cout << "通过有参构造函数初始化的盒子体积为:" << b2.Volume() << endl;
return 0;
}
#include <iostream>
using namespace std;
class Coordinate
{
public:
// 无参构造函数
// 如果创建一个类你没有写任何构造函数,则系统自动生成默认的构造函数,函数为空,什么都不干
// 如果自己显示定义了一个构造函数,则不会调用系统的构造函数
Coordinate()
{
c_x = 0;
c_y = 0;
}
// 一般构造函数
Coordinate(double x, double y):c_x(x), c_y(y){} //列表初始化
// 一般构造函数可以有多个,创建对象时根据传入的参数不同调用不同的构造函数
Coordinate(const Coordinate& c)
{
// 复制对象c中的数据成员
c_x = c.c_x;
c_y = c.c_y;
}
// 等号运算符重载
Coordinate& operator= (const Coordinate& rhs)
{
// 首先检测等号右边的是否就是等号左边的对象本身,如果是,直接返回即可
if(this == &rhs)
return* this;
// 复制等号右边的成员到左边的对象中
this->c_x = rhs.c_x;
this->c_y = rhs.c_y;
return* this;
}
double get_x()
{
return c_x;
}
double get_y()
{
return c_y;
}
private:
double c_x;
double c_y;
};
int main()
{
// 调用无参构造函数,c1 = 0,c2 = 0
Coordinate c1, c2;
// 调用一般构造函数,调用显示定义构造函数
Coordinate c3(1.0, 2.0);
c1 = c3; //将c3的值赋值给c1,调用"="重载
Coordinate c5(c2);
Coordinate c4 = c2; // 调用浅拷贝函数,参数为c2
cout<<"c1 = "<<"("<<c1.get_x()<<", "<<c1.get_y()<<")"<<endl
<<"c2 = "<<"("<<c2.get_x()<<", "<<c2.get_y()<<")"<<endl
<<"c3 = "<<"("<<c3.get_x()<<", "<<c3.get_y()<<")"<<endl
<<"c4 = "<<"("<<c4.get_x()<<", "<<c4.get_y()<<")"<<endl
<<"c5 = "<<"("<<c5.get_x()<<", "<<c5.get_y()<<")"<<endl;
return 0;
}
c1 = (1, 2)
c2 = (0, 0)
c3 = (1, 2)
c4 = (0, 0)
c5 = (0, 0)
请按任意键继续. . .
命名空间是ANSI C++引入的可以由用户命名的作用域,用来处理程序中常见的同名冲突。
在c++中有4层次的作用域:文件、函数、类、复合语句。在不同的作用域中可以定义名字相同的变量,互不干扰,便于系统区别他们。
如下:
class A
{
public:
void fun1();
private:
int i;
};
void A::fun1()
{
}
class B
{
public:
void fun1();
private:
int i;
};
void B::fun1()
{
}
这样,他们就不会发生混淆。
但是,一个大型的应用软件,往往不是由一个人独立完成的,而是由若干不同的人合作完成的,不同的人分别完成不同的部分,最后组成一个完整的程序。假如不同的人分别定义了类,放在了不同的文件中,在主函数的文件中需要使用这些类时,就用#include指令将这些头文件包含进来。由于头文件是由不同的人设计的,有可能在不同头文件中用了相同的名字来命名所定义的类或函数。这样,程序中就会出现名字冲突。
以下面的程序为例,在People A.h和People B.h分别定义类和函数:
//PeopleA.h
class Student
{
public:
Student(int n, char s, string name)
{
//.....
}
private:
int num;
string name;
char sex;
};
int fun(int a, int b)
{
return a + b;
}
//PeopleB.h
class Student
{
public:
Student(int n, char s, string name)
{
//.....
}
private:
int num;
char sex;
string name;
};
int fun(int a, int b)
{
return a + b;
}
假如在主程序中要用到People A.h中的Student函数,需要在头文件中包含People A.h,同时要用到People B.h中的Student函数,需要在头文件中包含People B.h,如果主程序如下:
#include <iostream>
#include "People A.h"
#include "People B.h"
int main()
{
Student stdu1(101, 18, "wang");
cout << fun(5, 3) << endl;
return 0;
}
这时程序就会出错,因为在预编译后,头文件中的 内容取代了对应的#include指令,这样就在同一个程序文件中出现了两个Student类和两个fun函数,显然是重复定义,这就是名字冲突,即在同一个作用域中有两个或者多个同名的实体。
所谓命名空间,实际上就是一个由程序设计者命名的内存区域。程序设计者可以根据需要制定一些有名字的空间域,把一些全局实体分别放在各个命名空间中,从而与其他全局实体分隔开来。如:
namespace AA
{
int a;
double b;
}
namespace是定义命名空间锁必须写的关键字,AA是自己制定的命名空间的名字。如果在程序中要使用a和b,必须加上命名空间名和作用域分辨符::,如AA::a,AA::b,这种用法称为命名空间限定。
命名空间的作用是建立一些互相分隔的作用域,把一些全局实体分隔开来,以免产生名字冲突。
如下程序为例:
//PeopleA.h
namespace PeopleA
class Student
{
public:
Student(int n, char s, string name)
{
//.....
}
private:
int num;
string name;
char sex;
};
int fun(int a, int b)
{
return a + b;
}
//PeopleB.h
namespace PeopleB
class Student
{
public:
Student(int n, char s, string name)
{
//.....
}
private:
int num;
char sex;
string name;
};
int fun(int a, int b)
{
return a + b;
}
#include <iostream>
#include "People A.h"
#include "People B.h"
int main()
{
PeopleA::Student stdu1(101, 18, "wang");
cout << PeopleA::fun(5, 3) << endl;
PeopleB::Student stdu1(101, 18, "wang");
cout << PeopleB::fun(5, 3) << endl;
return 0;
}
在引用命名空间成员时,要用命名空间名和作用域分辨符对命名空间成员进行限定,以区别不同的命名空间中的同名标识符。
即:命名空间名::命名空间成员名
c++提供了一些机制,能简化使用命名空间的使用:
可以为命名空间起一个别名,用来替代较长的命名空间名,如:
namespace PeopleA
可以用一个较短的别名替代它。如:
namespace PA = PeopleA
using后面的命名空间成员名必须是由命名空间限定的名字,如:
using AA::i;
如:using namespace AA;
声明了在本作用域中要用到命名空间AA中的成员,在使用该命名空间的任何成员时都不必再使用命名空间限定。
c++中可以声明无名的命名空间,如:
namespace
{
void fun()
{
//....
}
}
由于命名空间没有名字,在其他文件中显然无法引用,它只在本文件的作用域有效。若无名命名空间的成员fun函数的作用域为文件A,在文件A中使用无名命名空间的成员,不用也无法用命名空间名限定。
标准C++库中的所有标识符都是在一个名为std的命名空间中定义的,或者说标准头文件中的函数、类、对象和模板实在命名空间std中定义的。一般用using namespace语句对命名空间std进行声明,这样可以不必对每个命名空间成员一一进行处理,在文件的开头加入如下语句:
using namespace std;
这样,在std中定义和声明的所有标识符在本文件中都可以作为全局变量来使用。
由于namespace的概念,使用C++标准程序库的任何标识符时,可以有三种选择:
std::cout << std::hex << 3.4 << std::endl;
using std::cout;
using std::endl;
//以上程序可以写成
cout << std::hex << 3.4 << endl;
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
cout << hex << 3.4 << endl;
在所有的计算机程序中,一个基本的目标是操作一些数据,然后获得一些结果。为了操作这些数据,需要为这些数据分配一段内存,我们可以将这段内存称为变量。为了方便操作,以及程序可读性方面的考虑,需要使用一个有意义的名称来引用这段内存,这个名称就是变量名。
将名称和一段内存关联起来的工作可以分成两个阶段来进行,分别是变量的声明和定义。在变量声明的时候,只是引入了一个名称,该名称并没有和一段特定的内存关联。也就是说,在声明变量的时候,只是引入了一个助记符,并没有执行内存分配。在定义变量的时候,将前面声明过程中引入的名称关联到了一段特定的内存,内存的大小由变量的类型决定。也就是说,在定义变量的时候,真正执行了内存分配。在有的情况下,变量的声明和定义是需要分开进行的,如:全局变量的声明和定义,可以在多个文件中使用该变量;而在某些情况下,使用一个语句就可以完成变量的声明和定义,如:局部变量的声明和定义。只需要在一个文件中使用该变量。
在C++程序中,当声明并定义了一个变量以后,需要关注如下两个问题:
为了解决这两个问题,就需要引入作用域的概念。作用域是C++程序中的一段区域,一般用正反两个花括号来界定它的范围。**在同一个作用域范围内,一个名称只能唯一关联到一个实体,这个实体可以是变量,函数,类型,模版等。**也就是说,在同一作用域范围内,不同的实体必须对应不同的名称,绝对不允许出现两个不同的实体对应同一个相同的名称的情况。一个名称可以和不同作用域中的不同实体相对应。也就是说,对于同一个名称,在不同的作用域中可以重复使用。
在本文的后续部分,将对各种类型的作用域进行描述,并且介绍在作用域中进行名字解析的规则。
我们可以将整个C++程序(在程序中包括各种类型,函数,模版,变量等,并且分布在很多个*.cpp文件中)看成一个很大的整体区域。为了方便对C++程序中已经定义的各种类型,函数,模版,变量的管理,可以把这片大的区域划分成一片片小的命名区段。然后根据各个类型,函数,模版,变量的功能以及用途等,再把这些类型,函数,模版,变量等分别放置在不同的区段中。这些小的区段叫做作用域,C++程序支持四种形式的作用域,分别是:名字空间作用域,类域,局部作用域,语句作用域。
名字空间作用域就是程序员利用名字空间定义在C++程序中划分出来的一块比较大的程序区段。在该程序区段内部,可以定义类型,函数,模版,变量。名字空间作用域可以跨越多个*.cpp文件而存在。在名字空间作用域内部还可以继续定义其他的名字空间作用域,也就是说,名字空间作用域是可以互相嵌套的。
全局作用域是C++程序最外层的名字空间作用域,也是最大的名字空间作用域。**全局作用域天然存在于C++程序中,它不需要由程序员人为地定义。在全局作用域内部,可以包含其他的,由程序员定义的名字空间作用域,以及没有包含在其他名字空间作用域中的类型,函数,模版,变量。**在全局作用域中定义的变量是全局变量,在全局作用域中定义的函数是全局函数。
在C++程序中,每定义一个类就会引入一个类域。类体所包含的范围就是类域的范围,在类中定义的所有成员都属于该类域。类域位于名字空间作用域内部,该名字空间作用域可能是全局作用域,也可能是用户定义的名字空间作用域。
**每一个函数体内部都是一个局部作用域。该作用域起始于函数体的左花括号“{”,结束于函数体的右花括号“}”。**每一个函数都有一个独立的局部作用域。在局部作用域内定义的变量都是局部变量。
在C++程序中,当要求使用单个语句,但程序逻辑却需要不止一个单个语句的时候,我们可以使用复合语句。复合语句通常被称为块,是用花括号括起来的一些单个语句的集合。在复合语句花括号内部的区段也属于局部作用域。
有些语句存在控制结构,并且允许在控制结构中定义变量。如:
//示例一:
for ( int K = 0; K < 100;K++ )
cout << K; //该行语句属于语句作用域范围,K仅在这一行有效。
//示例二:
for (int K = 0; K < 100;K++)
{
… //其他代码
Cout << k; //花括号内部是复合语句,都属于语句作用域。K在整个花括号内有效。
… //其他代码
}
从控制语句的开始到控制语句结束这一段区域被称为语句作用域。在该控制结构中定义的变量,仅在该语句作用域内有效。如:示例二中,K在花括号内有效,或者示例一中,仅在语句“cout << K;”中有效。语句作用域是最小的作用域。
使用名字空间可以在一定程度上解决命名冲突的问题。假设没有名字空间,那么在C++程序中,所有的实体,如:函数,类型,变量,模版等,都必须被放置在全局域中作为全局实体而出现。在全局域中,这些实体必须具有唯一的名称,不允许存在多个实体同名的情况。因此,当在全局域中引入一些第三方开发的类库的时候,必须要保证第三方类库中命名的实体与全局域中命名的实体在命名方面不冲突。但是,这是很难保证的。为了解决这个问题,就引入了名字空间的概念。
第三方开发方在开发类库的时候,可以首先声明一个名字空间,每一个用户声明的名称空间都代表一个不同的名字空间域。在该名字空间中,可以包含嵌套其他的名称空间,以及函数,类型,变量,模版等的声明和定义。在该名称空间内部声明的实体被称为名称空间成员。用户在名字空间中声明的每个实体的名字必须是唯一的,不允许重名。因为在不同用户声明的名字空间中引入了不同的域,所以在这些由不同用户声明的名字空间中可以使用相同的名称。通过这种方式解决了命名冲突的问题。
在使用名字空间中的成员的时候,名字空间成员的名字会自动与该名字空间重合,或者说被其限定修饰。如:在名字空间A中声明的类B,它的名字是:A::B。
用户声明的名字空间以namespace关键字开头,后面是名字空间的名称。名字空间的范围以花括号界定,具体的格式如下:
namespace mySpace //mySpace是名字空间的名称
{
Class myClass { … }; //类定义
Int myFunction(int para1,int para2); //函数的声明
Extern double myVar; //变量的声明
}
在上面的示例中,声明了一个名称为mySpace的名字空间,该名字空间的作用域由花括号界定,在花括号内部的部分都属于该名字空间的作用域。在该名字空间中,定义了一个类:myClass,声明了一个函数:myFunction,以及一个变量myVar。它们都是该名字空间的成员。
用户声明的名字空间可以位于全局作用域中,也可以位于其他的名字空间的作用域中。在当前的作用域中,名字空间的名称是唯一的,不能与其类型的实体重名。
**在同一个作用域中,可以多次声明相同名称的名字空间。**在这种情况下,将会实现名字空间的累加。比如,A.h头文件和A.cpp源文件都位于全局作用域中,在这两个文件中分别声明如下的名字空间:
//A. h文件的代码实现:
namespace mySpace //在这里实现了函数和变量的声明,属于接口部分。
{
Int AddData (int para1,int para2); //函数的声明
Extern double myVar; //变量的声明
}
//B.cpp文件的代码实现:
Include “A.h”
namespace mySpace // 在这里实现了函数和变量的定义,属于实现部分。
{
Int AddData(int Para1,int Para2) //函数的定义
{
Return Para1+Para2;
}
Double myVar = 3.14; //变量的定义,并初始化。
}
在这里,存在这样一个规则:在同一个作用域中,如果新声明的一个名字空间的名称与前面声明过的名字空间的名称同名,那么这个后声明的名字空间就是前面声明的名字空间的累加,这两部分内容属于同一个名字空间;如果新声明的这个名字空间不与当前作用域中任何名字空间同名,那么就会定义一个新的名字空间。
在上面的示例中,A.h和A.cpp文件位于全局作用域中。在全局作用域中,两次声明的名字空间具有相同的名称:mySpace。因此,认为这两次声明的名字空间属于同一个名字空间。
通过对上面所描述的规则的使用,在程序设计的时候,可以根据需要,将名字空间的声明拆分成若干个部分来实现,只要这几个部分的声明都在同一个作用域中即可。这个规则的一个典型应用就是:实现接口和具体实现的分离。
在上面的示例中,我们将函数AddData和变量myVar的声明放在了A.h头文件中,而将它们的定义放在了另外一个A.cpp的源文件中。 A.h头文件实现的是函数库的接口的,而A.cpp文件中的内容则是针对接口的实现。因此,在程序设计和开发的时候,这两部分内容可以分别由不同的人在不同的时间实现。通过这种方式,实现了接口和具体实现分离的原则。
当定义了名字空间以后,就可以想名字空间中添加成员。这些被添加的成员可以是:类型,函数,变量,模版等。可以通过两种方式向名字空间中添加成员。
//方式一:在名字空间中直接完成成员的定义。成员的定义不在划分为声明和定义两部分。
Namespace mySpace
{
Double myVar = 3.14;
Int myFunction(int Para1)
{
Return Para1*10;
}
}
//方式二:在名字空间中先完成成员的声明,然后采用名字空间累加的方式,在其他部分完成成员的定义。这个“其他部分”,可以是其他的物理文件,也可以是同一个物理文件。
Namespace mySpace
{
Extern double myVar;
Int myFunction(int Para1);
}
Namespace mySpace
{
Double myVar = 3.14;
Int myFunction(int Para1)
{
Return Para1*10;
}
}
在上面的代码中,在定义了名字空间的同时(无论是采用累加方式,还是一次性完成),在名字空间内部完成了函数myFunction和变量myVar的定义。名字空间的定义和名字空间成员的定义同步完成。
//首先在一个文件中完成名字空间的定义,以及名字空间成员的声明。一般情况下,该文件为头文件(A.h)。
Namespace mySpace
{
Class myClass {….};//声明一个类型
myClass myFunction(myClass Para1);//声明一个函数,该函数返回myClass类型,并以myClass类型为参数。
}
在上面的代码中,完成了对名字空间mySpace的定义,同时在名字空间内部,完成了类myClass的定义,以及对函数myFunction的声明。接下来需要在其他地方,名字空间以外,完成对名字空间成员myFunction函数的定义。具体代码如下:
//实现函数myFunction定义的位置,可以是另外一个文件,一般为cpp文件,但是也可以在原来的头文件中(一般不会这么干)。
#include “A.h”
mySpace::myClass mySpace::myFunction(myClass Para1)
{
//下面完成函数的具体实现。
…
}
在上面的代码中,我们可以看到两处差异。一处是函数的返回值类型,myClass被名字空间mySpace限定修饰了;而在函数的参数类型处,myClass直接使用,没有被名字空间mySpace限定修饰。
这里存在这样一个规则:在函数的限定修饰名称“mySpace::myFunction”之后,直到方括号结束的区域都属于mySpace名字空间的作用域范围。也就是上面代码中的红色部分。
也就是说名字空间的作用域可能会有两部分组成,在大多数情况下,名字空间的作用域是由定义名字空间的时候,名字空间体的花括号界定的。但是,当在名字空间之外定义名称空间的成员的时候,在名字空间成员的限定修饰名之后直到结束花括号(” }”),或者分号(;)的部分都属于该名字空间作用域范围。
因此,在上面的代码中,参数的类型不需要被限定修饰,因为那个区域是属于名字空间作用域内的;而函数的返回类型必须要被限定修饰,因为那个区域不属于名字空间的作用域内。
另外还需要注意,在名字空间之外实现名字空间成员的定义的时候,要有一个前提,那就是:名字空间成员的声明必须在名字空间之内实现。
在C++程序中,使用名字空间的方式封装一些函数库或者类库的时候,一般情况下,通常的做法是这样的:**首先在一个头文件中定义一个名字空间,然后在该名字空间的定义中声明所有的名字空间成员,如:函数,类型,变量等。**之后将这个头文件引入到一个cpp文件中,并且在这个cpp文件中实现所有名字空间成员的定义。具体示例如下:
-----------------A.h------------------------------//头文件名称
namespace myCPlusPlusFunctionsV1.0
{
Class myClass { …//类成员的声明 }; //定义一个类型
Extern double myVar; //声明变量
Void DealClass(myClass*); //声明函数
}
-----------------A.cpp--------------------------//源文件
#include “A.h”
Namespace myCPlusPlusFunctionsV1.0
{
myClass:: myClass() { … // myClass构造函数的实现}
…
//其他myClass类成员的定义。
…
double myVar = 3.14;//变量的定义
void DealClass(myClass*pClass)
{
…//函数的具体实现。
}
}
在使用这些函数库或者类库的时候,首先需要将这个定义了该名字空间的头文件引入,然后开始使用该名字空间中的一些成员。在使用名字空间成员的时候,有三种方式:
------------------otherCPlusPlusFile.cpp-------------------------
#include “A.h”
Void main()
{
myCPlusPlusFunctionsV1.0::myClass *pClass = new myCPlusPlusFunctionsV1.0::myClass;
myCPlusPlusFunctionsV1.01::DealClass(pClass);
}
在上面的代码中,“::”是域操作符。名字空间成员的声明被隐藏在名字空间之中,所以,名称空间的成员名称不会与当前作用域中的对象实体名称产生冲突。在使用名字空间成员的时候,可以使用名字空间名+域操作符+名字空间成员名称的方式将名字空间成员引入到当前的作用域中。否则,在当前作用域中,编译器不会找到名字空间的成员。
域操作符也可以被用来引用全局作用域的成员。因为全局作用域没有名称,所以使用如下的符号:
::member_name
指向全局名字空间的成员。当全局名字空间成员的名称被局部作用域中的名字隐藏的时候,但又需要在局部作用域中使用全局成员的时候,就可以使用这种引用方式。
在上面的示例中,名字空间的名称“myCPlusPlusFunctionsV1.0”比较长,在使用的时候,可能会不方便,因此,C++在处理这个问题的时候,引入了名字空间别名的概念。
所谓名字空间别名就是为已经定义的名字空间取一个其他的、替代性的名称,一帮情况下,这个名称是简短的,容易记忆的。具体使用方式如下:
------------------otherCPlusPlusFile.cpp-------------------------
#include “A.h”
Namespace myC++ = myCPlusPlusFunctionsV1.0;
Void main()
{
myC++::myClass *pClass = new myC++::myClass;
myC++::DealClass(pClass);
}
在上面的代码中,为名字空间“myCPlusPlusFunctionsV1.0”定义了一个别名“myC++”。之后在引用该名字空间成员的时候,就可以使用该别名。
定义名字空间别名的格式是:以关键字namespace开头,后跟名字空间的别名,并且等于前面定义好的名字空间的名称。
Using 声明的作用是:使一个名字空间成员在当前作用域中可见,可见的范围是从using声明的语句开始,直到当前作用域结束。如果在using声明语句之后,在当前作用域中又嵌套了其他的作用域,那么using声明在当前作用域中的嵌套作用域中也同样有效。
Using声明以关键字using开头,后跟名字空间的成员名称。该成员名称必须是名字空间名称+域操作符+名字空间成员名称形式的限定修饰名称。具体代码如下:
//名字空间的定义
Namespace mySpace
{
Int myFunction(int Para)//在名字空间中定义了一个函数
{
Return Para*10;
}
}
//在全局作用域中使用using声明,将名字空间成员名引入当前作用域。
Using mySpace::myFunction;
//开始使用名字空间的成员
Void main()
{
//也可以在此位置使用using声明,即在局部作用域使用using声明。
myFunction(10);//使用名字空间的成员。因为使用了using声明,所以不需要使用限定修饰的形式。名称myFunction从using声明开始,直到当前作用域结束。
}
在上面的代码中,首先定义了一个名字空间,并在名字空间中定义了一个函数。然后在全局作用域中使用了using声明。之后,在main函数中使用名字空间的成员函数myFucntin。
**可以在全局作用域,名字空间作用域,局部作用域中使用using声明。**在使用了using 声明以后,一次只能从源名字空间向当前作用域中引入一个名字空间成员,但可以多次使用using声明。如果该名字空间成员是函数,并且在该名字空间中具有多个重载,那么在使用using声明的时候,所有的重载函数都会被引入到当前的作用域中。**被引入的名字空间成员名只在当前作用域中有效,并且名称唯一。这个被引入的名字空间成员名会隐藏当前作用域外围作用域中的同名名称,也会被当前作用域的嵌套作用域中的同名名称隐藏。**具体情况见如下代码:
namespace mySpace
{
Int myIntVar = 10;//定义一个整型变量。名字空间成员。
}
Int myIntVar = 100;//全局变量
Int main()
{
Using mySpace::myIntVar;//该using声明隐藏了全局变量myIntVar。
Int k = 10;
K = k + myIntVar;//使用的是名字空间的成员变量,所以k的值等于20.
K = K + ::myIntVar;//这里使用的是全局变量,所以k的值等于110.
{
Int myIntVar = 50;//在此语句作用域中声明的变量隐藏了前面using声明中引入的变量。
Int a = myIntVar ;//a = 50
Int b = ::myIntVar;//b = 100;
Int C = mySpace::myIntVar;//c = 10;
}
}
使用using声明将名字空间的成员引入到当前作用域的时候,除了重载函数以外,被引入的成员名称不能与当前作用域中定义的对象实体重名,否则会引起错误。
Using指示符以关键字using 开头后跟关键字namespace,最后是名字空间的名称。该名字空间的名称必须在前面已经定义。其作用域从using指示符开始,直到当前作用域结束。使用using指示符以后,将会把名字空间中的所有成员引入到当前作用域。具体的代码如下:
//定义名字空间
Namespace mySpace
{
Int myFunction(int Para)
{
Return Para*10;
}
Int myVar = 100;
}
//使用using指示符,将名字空间的所有成员引入到当前作用域。目前是全局作用域。
Using namespace mySpace;
Void main()
{
Int k = myVar + 10;//使用using指示符以后,可以直接使用名字空间中的成员,就好像该//名字空间的成员在当前作用域中定义的一样,不需要限定修饰。
myFunction(k);
}
在上面的代码中,首先定义了一个名字空间mySpace,同时在名字空间中定义了一个函数myFunction,以及一个变量myVar。然后使用using指示符将该名字空间中的成员引入到了全局作用域中。之后,在main函数中使用名字空间的成员,使用的时候,不需要限定修饰,就好像使用当前名字空间中定义的成员一样。
在当前作用域使用using指示符以后,被引用的名字空间将与当前的作用域合并,名字空间中的成员就好像在当前作用域被定义一样。因此,在当前作用域中,不能定义与名称空间成员重名的对象。否则会因此错误。
在名字空间的概念被提出之前,在C++中就已经存在了大量的库函数。这些库函数有的是标注C形式的,也有的是标准C++形式的。在声明这些库函数的时候,按照其功能和类别,它们被划分到很多不同的头文件中,如:iostream.h,complox.h,stdio.h。当名字空间的概念被提出之后,这些库函数被重新整理,将它们的声明和定义放到了名称空间名称为std的名称空间中。它们被称为标准C++库。
但是为了向前兼容以前实现的C++程序,在对这些库函数进行整理的时候,创建了新的头文件,并采用了新的命名规则,以区分原有的库函数。具体的处理方式描述如下:
对于支持C++的头文件,如:<iostream.h>,在被重新整理之后,它的名称为去掉了头文件的扩展名。新的头文件所包含的功能与旧头文件基本相同,但是它们在std名字空间中; 对于支持C标准的头文件,如:<stdio.h>,在被重新整理之后,它的名称为,在名称的前面加上了前缀字符“C”,并去掉扩展名。新的头文件所包含的功能与旧的头文件基本相同,但是它们在std名字空间中。 原有旧的C++标准头文件,如<iostream.h>,依然被支持,它们不在名字空间std中; 原有旧的C标准头文件,如<stdio.h>,依然被支持,它们不在名字空间std中。
在用户声明的名字空间中还可以继续嵌套其他的名字空间,通过这种分层次的名字空间的结构可以改善函数库的代码组织结构。具体代码如下:
Namespace myFirstSpace
{
Int myVar = 10;
Namespace mySecondSpace
{
int dlVar = 314;
Int myVar = 100;//它会隐藏外围名字空间声明的变量。
}
}
只要需要,名字空间的嵌套可以一直向下持续下去。在名字空间嵌套的时候,外围名字空间声明的变量可能会被里面嵌套的名字空间声明的同名变量隐藏。在使用嵌套名字空间成员的时候,有三种方式,具体情况如下:
//第一种形式:限定修饰名称形式
Int a = MyFirstSpace::mySecondSpace::dlVar;
//第二中形式:using声明的形式:
Using myFirstSpace::mySecondSpace::dlVar;
Int a= dlVar;
//第三中形式:using指示符形式:
Using namespace myFirstSpace::mySecondSpace;
Int a = dlVar;
使用未命名的名字空间,可以定义文件作用域。具有文件作用域的名字空间只在定义它的文件中有效,在其他文件中访问不到该作用域。
未命名名字空间的定义格式如下:
----------------------------A.cpp--------------------------
Namespace
{
Int a = 10;
Void myFunction(int Para)
}
//使用未命名名字空间中的成员
Void main()
{
myFunciton(a);//直接使用,不需要限定修饰。
}
在使用未命名名字空间中的成员的时候,可以直接使用,不需要限定修饰。未命名名字空间中的成员只能在定义它的文件中使用,在其他文件中是无法访问的。
注意点:
在C语言中,我们知道有static静态变量,生命周期与作用域都跟普通变量有所不同。而在C++的类中,也有静态成员变量同时还有静态成员函数,先来看看C++中静态成员变量与静态成员函数的语法:
#include <iostream>
#include <string>
using namespace std;
class test
{
private:
static int m_value; //定义类的静态成员变量
public:
static int getValue() //定义类的静态成员函数
{
return m_value;
}
};
int test::m_value = 12; //类的静态成员变量需要在类外分配内存空间
int main()
{
test t;
cout << t.getValue() << endl;
system("pause");
}
以上代码,我们在test类中分别定义了一个静态成员变量与静态成员函数,首先来看下静态成员变量:
针对静态成员变量的以上几点,我们把上边的代码修改如下,用于统计当前对象的个数:
#include <iostream>
#include <string>
using namespace std;
class test
{
private:
static int m_value; //定义私有类的静态成员变量
public:
test()
{
m_value++;
}
static int getValue() //定义类的静态成员函数
{
return m_value;
}
};
int test::m_value = 0; //类的静态成员变量需要在类外分配内存空间
int main()
{
test t1;
test t2;
test t3;
cout << "test::m_value2 = " << test::getValue() << endl; //通过类名直接调用公有静态成员函数,获取对象个数
cout << "t3.getValue() = " << t3.getValue() << endl; //通过对象名调用静态成员函数获取对象个数
system("pause");
}
这样我们就直接能通过类名去访问静态成员函数,获取对象个数,不通过任何对象。
静态成员是解决同一个类的不同对象之间数据和函数共享问题.
静态数据成员
1.用关键字static声明
2.该类的所有对象维护该成员的同一个拷贝。
3.必须在类外定义和初始化,用(::)来指明所属的类。
静态成员函数
1.类外代码可以使用类名和作用域操作符来调用静态成员函数。 2.静态成员函数只能引用属于该类的静态数据成员或静态成员函数。
#include <iostream>
using namespace std;
class Point
{
public:
Point(int xx=0, int yy=0) {X=xx; Y=yy; countP++; }
Point(Point &p);
int GetX() {return X;}
int GetY() {return Y;}
void GetC() {cout<<" Object id="<<countP<<endl;}
private:
int X,Y;
static int countP; //静态数据成员声明
};
Point::Point(Point &p)
{
X=p.X;
Y=p.Y;
countP++; //所有对象共同维护同一个countP
}
int Point::countP=0; //静态数据成员定义和初始化,使用类名限制
void main()
{
Point A(4,5);
cout<<"Point A,"<<A.GetX()<<","<<A.GetY();
A.GetC();
Point B(A);
cout<<"Point B,"<<B.GetX()<<","<<B.GetY();
B.GetC();
}
注意:static int countP;的访问权限是private,为什么在类外直接初始化?因为countP是静态的,被允许在类外初始化。
共有的静态成员函数,可以通过类名或对象名来调用. 一般的非静态成员函数只能通过对象名来调用.
#include <iostream>
using namespace std;
class Point //Point类声明
{
public: //外部接口
Point(int xx=0, int yy=0)
{X=xx;Y=yy;countP++;}
Point(Point &p); //拷贝构造函数
int GetX() {return X;}
int GetY() {return Y;}
static void GetC()
{cout<<" Object id="<<countP<<endl;}
private: //私有数据成员
int X,Y;
static int countP;
};
Point::Point(Point &p)
{
X=p.X;
Y=p.Y;
countP++;
}
int Point::countP=0;
void main()
{
Point A(4,5);
cout<<"Point A,"<<A.GetX()<<","<<A.GetY();
A.GetC(); //对象名引用
Point B(A);
cout<<"Point B,"<<B.GetX()<<","<<B.GetY();
Point::GetC(); //类名引用
}
#include<iostream>
using namespace std;
class Application
{
public:
static void f();
static void g();
private:
static int global;
};
int Application::global=0;
void Application::f()
{
global=5;
}
void Application::g()
{
cout<<global<<endl;
}
int main()
{
Application::f();
Application::g();
return 0;
}
静态成员函数在C++中的作用很强大,包括后边的介绍的单例模式、二阶构造模式,都用到静态成员函数及静态成员变量。
下边的图为静态成员函数与普通成员函数的比较

friend。(友元类的所有成员函数都自动成为友元函数)分类:
友元函数:在类声明中由关键字friend修饰的非本类的函数,在它的函数体中能够通过对象名访问private 和protected成员。
访问对象中的成员必须通过对象名。
使用友元函数计算两点距离
#include <iostream>
#include <cmath>
using namespace std;
class Point
{
public: //外部接口
Point(int xx=0, int yy=0) {X=xx;Y=yy;}
int GetX() {return X;}
int GetY() {return Y;}
friend float Distance(Point &a, Point &b);
private: //私有数据成员
int X,Y;
};
float Distance( Point& a, Point& b)
{
double dx=a.X-b.X;
double dy=a.Y-b.Y;
return sqrt(dx*dx+dy*dy);
}
int main()
{
Point p1(3.0, 5.0), p2(4.0, 6.0);
double d = Distance(p1, p2);
cout<<"The distance is "<<d<<endl;
return 0;
}
若A类为B类的友元类,则A类的所有成员函数都是B类的友元函数,都能访问对方类的private 和protected成员。(反之不成立)
class B
{
…
friend class A; //声明A为B的友元类
…
}
class A
{
friend class B;
public:
void Display
{
cout<<x<<endl;
}
int Getx()
{
return x;
}
//其它成员略
private:
int x;
};
class B
{
public:
void Set(int i);
void Display();
private:
A a;
};
void B::Set(int i)
{
a.x=i;
}
void B::Display()
{
a.Display();
}
B是A的友元,B的成员函数可以访问A类对象的私有数据。
注意:
定义Boat与Car两个类,二者都有weight属性,定义二者的一个友元函数otalWeight() 计算二者的重量和。
class Boat
{
private:
int weight;
public:
Boat(int j){weight = j;}
friend int totalWeight(Car &aCar, Boat &aBoat);
};
class Car
{
private:
int weight;
public:
Car(int j){weight = j;}
friend int totalWeight(Car &aCar, Boat &aBoat);
};
#include "car.h"
using namespace std;
int totalWeight(Car &aCar, Boat &aBoat)
{
return aCar.weight + aBoat.weight;
}
int main()
{
Car c1(4);
Boat b1(5);
cout << totalWeight(c1, b1) << endl;
return 0;
}
const
#include<iostream>
using namespace std;
void display(const double& r);
int main()
{
double d(9.5);
display(d);
return 0;
}
void display(const double& r)
//常引用做形参,在函数中不能更新 r所引用的对象。
{ cout<<r<<endl; }
class A
{
public:
A(int i,int j) {x=i; y=j;}
...
private:
int x,y;
};
const A a(3,4); //a是常对象,不能被更新
常成员函数
常成员函数举例
#include<iostream>
using namespace std;
class R
{
public:
R(int r1, int r2)
{
R1=r1;R2=r2;
}
void print();
void print() const;
private:
int R1,R2;
};
void R::print()
{
cout<<R1<<":"<<R2<<endl;
}
void R::print() const
{
cout<<R1<<";"<<R2<<endl;
}
void main()
{
R a(5,4);
a.print();
const R b(20,52);
b.print();
}
#include <iostream>
using namespace std;
class A
{
public:
A(int i);
void print();
const int& r;
private:
const int a;
static const int b;
};
const int A::b=10;
A::A(int i):a(i),r(a) {}
void A::print()
{
cout<<a<<":"<<b<<":"<<r<<endl;
}
int main()
{
A a1(100),a2(0);
a1.print();
a2.print();
return 0;
}
决定一个声明放在源文件中还是头文件中的一般原则是:
//不使用条件编译的头文件
//main.cpp
#include "file1.h"
#include "file2.h"
void main()
{
…
}
//file1.h
#include "head.h"
…
//file2.h
#include "head.h"
…
//head.h
…
class Point
{
…
}
…
//使用条件编译的头文件
//head.h
#ifndef HEAD_H
#define HEAD_H
…
class Point
{
…
}
…
#endif
//Point.h
class Point{
public:
Point();
//其他类成员
};
//Point.cpp
#include "Point.h"
Point::Point(){
//类成员函数实现
}
//main.cpp
#include "Point.h"
int main(){
Point p;
//主函数代码
return 0;
}
而当我们有多个文件的时候,我们就需要手动地来进行编译与连接了——我们需要把主函数所需要的类定义与实现文件都编译成目标代码文件,然后将其与主函数编译的目标代码文件连接起来,这样才能得到一个可执行文件。
所谓对象数组,指每一个数组元素都是对象的数组,即若一个类有若干个对象,我们把这一系列的对象用一个数组来存放。对象数组的元素是对象,不仅具有数据成员,而且还有函数成员。
定义一个一维数组的格式如下: 类名 数组名[下标表达式]
与基本数据类型的数组一样,在使用对象数组时也只能访问单个数组元素,其一般形式为: 数组名[下标].成员名
在建立数组时,同样要调用构造函数。有几个数组元素就要调用几次构造函数。
原则:
如果构造函数只有1个参数,在定义对象数组时可以直接在等号后面的花括号内提供实参来实现初始化。
例 1: 用只有1个参数的构造函数给对象数组赋值。
#include<iostream>
using namespace std;
class exam{
public:
exam(int n){ //只有1个参数的构造函数
x=n;
}
int get_x(){
return x;
}
private:
int x;
};
int main(){
exam ob1[4]={11,22,33,44}; //用只有1个参数的构造函数给对象数组赋值
for(int i=0;i<4;i++)
cout<<ob1[i].get_x()<<" ";
return 0;
}
当各个元素的初始值相同时,可以在类中定义不带参数的构造函数或带有默认参数值的构造函数;当各元素的对象的初值要求为不同时,需要定义带参数(无默认值) 的构造函数。
例2: 用不带参数和带1个参数的构造函数给对象数组赋值。
#include<iostream>
using namespace std;
class exam{
public:
exam(){
x=123;
}
exam(int n){
x=n;
}
int get_x(){
return x;
}
private:
int x;
};
int main(){
exam ob1[4]={11,22,33,44};
exam ob2[4]={55,66};
exam ob3[4];
for(int j=0;j<4;j++){
cout<<ob1[j].get_x()<<" ";
}
cout<<endl;
for(int j=0;j<4;j++){
cout<<ob2[j].get_x()<<" ";
}
cout<<endl;
for(int j=0;j<4;j++){
cout<<ob3[j].get_x()<<" ";
}
return 0;
}
如果构造函数有多个参数,在定义对象数组时只需在花括号中分别写出构造函数并指定实参即可实现初始化。
例3: 用带有2个参数的构造函数给对象数组赋值。
#include<iostream>
#include<cmath>
using namespace std;
class Complex{
public:
Complex(double r=0.0,double i=0.0):real(r),imag(i){}
~Complex(){
cout<<"Destructor called."<<endl;
}
double abscomplex(){
double t;
t=real*real+imag*imag;
return sqrt(t);
}
private:
double real;
double imag;
};
int main(){
Complex com[3]={ //定义对象数组
Complex(1.1,2.2), //调用构造函数,为第1个对象数组元素提供实参1.1和2.2
Complex(3.3,4.4), //调用构造函数,为第2个对象数组元素提供实参3.3和4.4
Complex(5.5,6.6) //调用构造函数,为第3个对象数组元素提供实参5.5和6.6
};
cout<<"复数1的绝对值是:"<<com[0].abscomplex()<<endl;
cout<<"复数1的绝对值是:"<<com[1].abscomplex()<<endl;
cout<<"复数1的绝对值是:"<<com[2].abscomplex()<<endl;
return 0;
}
继承和派生
继承机制: 是类型层次结构设计中实现代码的复用重要手段。
派生: 保持原有类特性的基础上进行扩展,增加新属性和新方法,从而产生新的类型。
在面向对象程序设计中,继承和派生是构造出新类型的过程。呈现类型设计的层次结构,体现了程序设计人员对现实世界由简单到复杂的认识过程。
C++ 通过类派生( class derivation)的机制来支持继承。被继承的类称为基类(base class) 或 超类(superclass),新产生的类为派生类(derived class) 或 子类(subclass)。基类和派生类的集合称作类继承层次结构(hierarchy)。
由基类派生出,派生类的设计形式为:
class 派生类名:访问限定符 基类名
{
private:
成员表1; //派生类增加或替代的私有成员
public:
成员表2; //派生类增加或替代的公有成员
protected:
成员表3; //派生类增加或替代的保护成员
};//分号不可少
示例:
定义一个person基类:
class Person
{
private:
char _idPerson[20]; //身份证号,18位数字
char _name[8]; //姓名
int _age;
public:
Person(){
_idPerson[0] = '0';
_name[0] = '0';
_age = 1;
}
Person(const char* id, const char* name, int age){
strcpy_s(_idPerson, 20, id);
strcpy_s(_name, name);
_age = age;
}
~Person() {}
void Dance() const
{
cout<<" 跳舞"<<endl;
}
void PrintPersonInfo() const
{
cout << "身份证号: " << _idPerson << 't';
cout << "姓名: " << _name << "t";
cout << "年龄: " << _age << endl;
}
};
定义一个学生派生类,public的形式继承person基类
// 派生类 访问限定符 基类
class Student : public Person
{
private:
char _snum[10];
float _score;
public:
void Study() const
{
cout<<"学生学习!"<<endl;
}
Student():Person()
{
_snum[0] = '0';
_score = 0.0;
}
Student(const char * id,const char * name,int age,const char * num):Person(id,name,age){
strcpy_s(_snum,10,num);
_score = 0.0;
}
~Student() {}
void PrintStudentInfo() const
{
PrintPersonInfo();
cout << "学号: " << _snum << "t" << "成绩: " << _score << endl;
}
};
派生类sudent可以继承基类person的成员和方法
int main()
{
Student studx("61010100010102345", "yhping", 23, "2022001");
return 0;
}
派生类student可以调用基类person 的public和protected方法
总结
派生反映了事物之间的联系,事物的共性与个性之间的关系。派生与独立设计若干相关的类,前者工作量少,重复的部分可以从基类继承来,不需要单独编程。继承是类型设计层面上的复用。
派生类的一般定义语法为:
class 派生类名:继承方式 基类名1,继承方式 基类名2,……继承方式 基类n
{
派生类成员声明;
};
一个派生类可以同时有多个基类,这种情况称为多继承。
一个派生类可以只有一个基类,这种情况称为单继承,是多继承的一个特例。
注意点:
class employee //基类
{
protected:
int individualEmpNo; //个人编号
int grade; //级别
float accumPay; //月薪
static int employeeNo; //本公司职员编号目前最大值
public:
employee(); //构造函数
~employee(); //析构函数
void pay(); //计算月薪函数
void promote(int); //升级函数
void SetaccumPay (float pa); //设置月薪函数
int GetindividualEmpNo(); //提取编号函数
int Getgrade(); //提取级别函数
float GetaccumPay(); //提取月薪函数
};
class technician:public employee //兼职技术人员类
{
private:
float hourlyRate; //每小时酬金
int workHhours ; // 当月工作时数
public:
technician(); //构造函数
void SetworkHours(int wh); //设置工作时间
void pay(); //计算月薪函数
};
不同继承方式的影响主要体现在:
三种继承方式:
公有继承特点:
公有继承举例:
class Point //基类Point类的声明
{
public: //公有函数成员
void InitP(float xx=0, float yy=0)
{X=xx;Y=yy;}
void Move(float xOff, float yOff)
{X+=xOff;Y+=yOff;}
float GetX() {return X;}
float GetY() {return Y;}
private: //私有数据成员
float X,Y;
};
class Rectangle: public Point //派生类声明
{
public: //新增公有函数成员
void InitR(float x, float y, float w, float h)
{InitP(x,y);W=w;H=h;} //调用基类公有成员函数
float GetH() {return H;}
float GetW() {return W;}
private: //新增私有数据成员
float W,H;
};
#include<iostream>
#include<cmath>
using namecpace std;
int main()
{
Rectangle rect;
rect.InitR(2,3,20,10); //通过派生类对象访问基类公有成员
rect.Move(3,2);
cout<<"The data of rect(X,Y,W,H):"<<endl;
cout<<rect.GetX()<<','
<<rect.GetY()<<','
<<rect.GetH()<<','
<<rect.GetW()<<endl;
return 0;
}
Rectangle类中的成员函数及对象可以访问基类的公有和保护类型的成员,但不能访问基类的私有成员。Rectangle类继承了Point类的成员,也就实现了代码的重用,同时,通过新增成员,加入自身的独有特征,达到程序的扩充。
私有继承特点:
#include<iostream>
#include<cmath>
using namecpace std;
class Rectangle: private Point //派生类声明
{
public: //新增外部接口
void InitR(float x, float y, float w, float h)
{InitP(x,y);W=w;H=h;} //访问基类公有成员
void Move(float xOff, float yOff) {Point::Move(xOff,yOff);}
float GetX() {return Point::GetX();}
float GetY() {return Point::GetY();}
float GetH() {return H;}
float GetW() {return W;}
private: //新增私有数据
float W,H;
};
int main()
{ //通过派生类对象只能访问本类成员
Rectangle rect;
rect.InitR(2,3,20,10);
rect.Move(3,2);
cout<<rect.GetX()<<',' <<rect.GetY()<<','
<<rect.GetH()<<','<<rect.GetW()<<endl;
return 0;
}
多态性是指同一个函数在不同的类中具有不同的功能。
多态性( polymorphism ) 是面向对象程序设计的重要特征。 多态是指同样的消息被不同的类型的对象接收时导致的不同的行为。 所谓消息就是指对类的成员函数的调用,不同的行为就是指不同的实现。
联编
联编也称绑定,是指在一个源程序经过编译链接成为可执行文件的过程中,将可执行代码“缝合”在一起的步骤。其中在程序运行前就完成的称为静态联编(前期联编);在程序运行时完成的称为动态联编(后期联编)。
静态联编支持的多态性称为编译时多态(静态多态),通过函数重载或函数模板实现;动态联编支持的多态性称为运行时多态(动态多态),通过虚函数表实现。
从系统实现的角度看,多态性分为以下两类:
通过函数重载或函数模板实现静态多态性
首先,以C语言实现int, double,char 类型的比较大小函数为例:
int my_max_i(int a,int b) { return a > b ? a : b;}
double my_max_d(double a,double b) { return a > b ? a : b;}
char my_max_c(char a,char b) { return a > b ? a : b;}
观察这些函数可以发现:
这些函数都执行了相同的一般性动作; 都返回两个形参中的最大值; 从用户的角度来看, 只有一种操作 ,就是判断最大值, 至于怎样完成其细节,函数的用户一点也不关心。
这种词汇上的复杂性不是 “判断参数中的最大值” 问题本身固有的, 而是反映了程序设计环境的一种局限性: 在同一个域中出现的名字必须指向一个唯实体(函数体) 。
这种复杂性给程序员带来了一个实际问题 ,他们必须记住或查找每一个函数名字。
函数重载把程序员从这种词汇复杂性中解放出来。
在C++中可以为两个或两个以上的函数提供相同的函数名称,只要参数类型不同,或参数类型相同而参数的个数不同, 称为函数重载。
// my_max + 参数表
int my_max(int a,int b)
{
return a > b ? a : b;
}
char my_max(char a,char b)
{
return a > b ? a : b;
}
double my_max(double a,double b)
{
return a > b ? a : b;
}
//每个同名函数的参数表是惟一
int main()
{
int ix = my_max(12,23);
double dx = my_max(12.23,34.45);
char chx = my_max('a','b');
return 0;
}
编译器的工作:
当一个函数名在同一个域中被声明多次时,编译器按如下步骤解释第二个(以及后续的)的声明。
如果两个函数的参数表中参数的个数或类型或顺序不同,则认为这两个函数是重载。
例如:
// 重载函数
void print(int a,char b);
void print(char a,int b);
int my_max(int a,int b)
{
return a > b ? a : b;
}
unsigned int my_max(int a,int b) // error;
{
return a > b ? a : b;
}
int main()
{
int ix = my_max(12,23);
unsigned int = my_max(12,23); // error;
return 0;
}
// 声明同一个函数
int my_add(int a,int b);
int my_add(int x,int y);
void Print(int *br,int n);
void Print(int *br,int len = 10);
typedef unsigned int u_int;
int Print(u_int a)
int Print(unsigned int b);
void fun(int a){ }
void fun(const int a) { }
void fun(int *p) {}
void fun(const int *p) {}
void fun(int &a) {}
void fun(const int &a) {}
void fun(int a){}
void fun(int a,int b){}
void fun(int a ,int b = 10);
“C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。
修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。
不要将不同功能的函数定义为重载函数。
返回类型不同的函数是否可以重载?
两个方法方法名相同,返回值类型不同不可以构成函数重载。
函数重载是同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成不同的功能。不能只有函数返回值类型不同。
运算符重载是对已有的运算符赋予多重含义
必要性:
C++中预定义的运算符其运算对象只能是基本数据类型,而不适用于用户自定义类型(如类),因此运算符重载的目的是,为用户自定义类型提供预定义的运算符。
实现机制:
将指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参。编译系统对重载运算符的选择,遵循函数重载的选择原则。
. .* :: ?:运算符重载的方式有两种:
重载为类成员函数。
函数类型 operator 运算符(形参表)
{
函数体;
}
重载为友元函数。
friend 函数类型 operator 运算符(形参表)
{
函数体;
}
运算符函数是类中声明的函数,其名称与运算符相同。
返回类型 operator 运算符(形参表)
{
函数体;
}
如果要重载 B 为类成员函数,使之能够实现表达式 oprd1 B oprd2,其中 oprd1 为A 类对象,则 B 应被重载为 A 类的成员函数,形参类型应该是 oprd2 所属的类型。
经重载后,表达式 oprd1 B oprd2 相当于oprd1.operator B
问题举例
(对复数类进行运算符重载)
#include<iostream>
using namespace std;
class complex //复数类声明
{
public: //外部接口
complex(double r=0.0,double i=0.0){real=r;imag=i;}complex operator + (complex c2); //+重载为成员函数
complex operator - (complex c2); //-重载为成员函数
void display(); //输出复数
private: //私有数据成员
double real; //复数实部
double imag; //复数虚部
};
complex complex::operator +(complex c2)
//重载函数实现
{
complex c;
c.real=c2.real+real;
c.imag=c2.imag+imag;
return complex(c.real,c.imag);
}
complex complex::operator -(complex c2) //重载函数实现
{
complex c;
c.real=real-c2.real;
c.imag=imag-c2.imag;
return complex(c.real,c.imag);
}
void complex::display()
{
cout<<"("<<real<<","<<imag<<")"<<endl;
}
int main() //主函数
{
complex c1(5,4),c2(2,10),c3; //声明复数类的对象
cout<<"c1="; c1.display();
cout<<"c2="; c2.display();
c3=c1-c2; //使用重载运算符完成复数减法
cout<<"c3=c1-c2=";
c3.display();
c3=c1+c2; //使用重载运算符完成复数加法
cout<<"c3=c1+c2=";
c3.display();
}
如果要重载 U 为类成员函数,使之能够实现表达式 U oprd,其中 oprd 为A类对象,则 U 应被重载为 A 类的成员函数,无形参。 经重载后,表达式 U oprd 相当于 oprd.operator U()
如果要重载 ++或--为类成员函数,使之能够实现表达式 oprd++ 或 oprd-- ,其中 oprd 为A类对象,则 ++或-- 应被重载为 A 类的成员函数,且具有一个 int 类型形参。
经重载后,表达式 oprd++ 相当于 oprd.operator ++(0)
问题举例:
运算符前置++和后置++重载为时钟类的成员函数。
前置单目运算符,重载函数没有形参,对于后置单目运算符,重载函数需要有一个整型形参。
操作数是时钟类的对象。
实现时间增加1秒钟。
#include<iostream>
using namespace std;
class Clock //时钟类声明
{
public: //外部接口
Clock(int NewH=0, int NewM=0, int NewS=0);
void ShowTime();
Clock& operator ++(); //前置单目运算符重载
Clock operator ++(int); //后置单目运算符重载
private: //私有数据成员
int Hour,Minute,Second;
};
Clock& Clock::operator ++() //前置单目运算符重载函数
{
Second++;
if(Second>=60)
{
Second=Second-60;
Minute++;
if(Minute>=60)
{
Minute=Minute-60;
Hour++;
Hour=Hour%24;
}
}
return *this;
}
//后置单目运算符重载
Clock Clock::operator ++(int)
{ //注意形参表中的整型参数
Clock old=*this;
++(*this);
return old;
}
int main()
{
Clock myClock(23,59,59);
cout<<"First time output:";
myClock.ShowTime();
cout<<"Show myClock++:";
(myClock++).ShowTime();
cout<<"Show ++myClock:";
(++myClock).ShowTime();
}
如果需要重载一个运算符,使之能够用于操作某类对象的私有成员,可以此将运算符重载为该类的友元函数。
函数的形参代表依自左至右次序排列的各操作数。
后置单目运算符 ++和--的重载函数,形参列表中要增加一个int,但不必写形参名。
oprd1 B oprd2等同于operator B(oprd1,oprd2 )B oprd 等同于operator B(oprd )oprd B等同于operator B(oprd,0 )问题举例: 将+、-(双目)重载为复数类的友元函数。两个操作数都是复数类的对象。
#include<iostream>
using namespace std;
class complex //复数类声明
{
public: //外部接口
complex(double r=0.0,double i=0.0)
{ real=r; imag=i; } //构造函数
friend complex operator + (complex c1,complex c2);
//运算符+重载为友元函数
friend complex operator - (complex c1,complex c2);
//运算符-重载为友元函数
void display(); //显示复数的值
private: //私有数据成员
double real;
double imag;
};
complex operator +(complex c1,complex c2) //运算符重载友元函数实现
{ return complex(c2.real+c1.real, c2.imag+c1.imag);
}
complex operator -(complex c1,complex c2) //运算符重载友元函数实现
{ return complex(c1.real-c2.real, c1.imag-c2.imag);
} // 其它函数和主函数同例8.1
以友元函数形式重载Complex的运算符<<
<<运算符只能用友元函数的形式重载,因为它的第一个操作数的类型为ostream,是标准库的类型,无法向其中添加成员函数。
cout<<c3相当于operator<<(cout,c3);
程序设计中经常会用到一些程序实体:它们的实现和所完成的功能基本相同,不同的仅仅是所涉及的数据类型不同。而模板正是一种专门处理不同数据类型的机制。
模板是泛型程序设计的基础(泛型generic type——通用类型之意)。
函数、类以及类继承为程序的代码复用提供了基本手段,还有一种代码复用途径——类属类型(泛型),利用它可以给一段代码设置一些取值为类型的参数(注意:这些参数 的值是类型,而不是某类型的数据),通过给这些参数提供一些类型来得到针对不同类 型的代码。
下面可以看一个例子:
#include <iostream>
using namespace std;
void swapint(int* a, int* b)
{
int p = *a;
*a = *b;
*b = p;
}
void showint(int a, int b)
{
cout << "a=" << a << "," << "b=" << b << endl;
}
void swapfloat(float* a, float* b)
{
float p = *a;
*a = *b;
*b = p;
}
void showfloat(float a, float b)
{
cout << "a=" << a << "," << "b=" << b << endl;
}
int main()
{
int a1 = 3, b1 = 12;
swapint(&a1, &b1);
showint(a1, b1);
float a2 = 12.3, b2 = 45.1;
swapfloat(&a2, &b2);
showfloat(a2, b2);
return 0;
}
输出结果
a=12,b=3
a=45.1,b=12.3
这个例子就是实现了两种不同类型的两个数之间的交换,我们这里只是实现了两种,如 果要实现很多种,那代码量就非常大。如果可以只用一个函数和类来描述,那将会大大 减少代码量,并能实现程序代码的复用性,所以这里就要用到模板了.
值和类型是数据的两个主要特征,它们在C++中都可以被参数化。
数据的值可以通过函数参数传递,在函数定义时数据的值是未知的,只有等到函数调用时接收了实参才能确定其值。——这就是值的参数化。
数据的类型也可以通过参数来传递,在函数定义时可以不指明具体的数据类型,当函数调用时,编译器根据传入的实参自动推断数据类型。——这就是类型的参数化(把类型 定义为参数)
带有类型参数的一段代码,实际上代表了一类代码,可以实现代码的复用,常用模板来实现。
模板——是一段带有类型参数的程序代码,可以通过给这些参数提供一些类型来得到针对不同类型的具体代码。
模板是C++中支持泛型编程的重要方法,是实现代码复用的工具,是实现参数化多态(类型参数化)的手段,减轻了编程及维护的工作量和难度。
一个模板并非一个实实在在的类或函数,仅仅是一个类或函数的描述。模板是规则,通过模板可以演化出多个具体的类或函数。
在C++中,能带有类型参数的代码可以是函数和类,所以模板一般分为函数模板和类模板。
例如:我们求两个数相加的函数,实现int、float、double等多种类型数据相加
如果不使用模板的话,我们可以利用重载函数,定义多个同名函数实现函数重载, 但是采用函数重载,代码重复率高,可维护性极差。
这里可以采用函数模板,定义一个add函数模板,借助它实现多种类型数据相加
函数模板——实际上是定义一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)均被作为参数:不指定具体类型,而是用一个虚拟 的类型来代替(实际上是用一个标识符来占位)。凡是函数体相同的函数都可以用 这个模板来代替,在函数调用时根据传入的实参来逆推出真正的类型。这个通用函 数就称为函数模板。
通过函数模板实现类型参数化,即把类型定义为参数,从而实现代码复用。
可见:函数模板并不是一个可以直接使用的函数,它是可以产生多个函数的模板。
template <typename 形参名, typename 形参名...>
//模板头(模板说明)
返回值类型 函数名(参数列表)
//函数定义
{
函数体;
}
说明:
用函数模板实现多种类型数据的相加
#include <iostream>
using namespace std;
template<typename T>//模板头,template关键字告诉编译器开始泛型编程
T add(T t1, T t2)//类型参数化为T
{
return t1 + t2;
}
int main()
{
cout << add(12, 34) << endl;
cout << add(12.2,45.6) << endl;
return 0;
}
输出结果
46
57.8
当调用add()函数传入int型参数12和参数34时,形参T被替换成int,得到结果 为46
当传入float型参数12.2和参数45.6时,形参T被替换成float,得到结果为57.8
避免了为int型定义一个求和函数,再为float型定义一个求和函数,实现了代码复用.
利用模板实现了不同类型的变量相加,可以发现利用模板的话大大减少了代码量,提高了代码复用性.
#include <iostream>
using namespace std;
template< class T > //声明函数模板,或template <typename T>
void outputArray( const T *P_array, const int count )//定义函数体
{
for ( int i = 0; i < count; i++ )
cout << P_array[ i ] << " ";
cout << endl;
}
int main() //主函数
{
const int aCount = 8, bCount =8, cCount = 20;
int aArray[ aCount ] = { 1, 2, 3, 4, 5,6,7,8 }; //定义int数组
double bArray [ bCount ] = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7,8.8 };
//定义double数组
char cArray [ cCount ] = "Welcome to see you!"; //定义char数组
cout << " a Array contains:" << endl;
outputArray( aArray, aCount ); //调用函数模板
cout << " b Array contains:" << endl;
outputArray( bArray, bCount ); //调用函数模板
cout << " c Array contains:" << endl;
outputArray( cArray, cCount ); //调用函数模板
}
函数模板中声明了类型参数T,表示一种抽象的类型。当编译器检测到程序中调用函数模板outputArrary时,便用outputArrary的第一个 参数的类型替换掉整个模板定义中的T,并建立用来输出指定类型数组的一个完整的函数,然后再编译这个新建的函数。
注意:对函数模板的调用应使用实参推演来进行。
add(2,3) 或 int a=2,b=3; add(a,b)
//对,编译器会根据传入的实际参数2、3推演出传入的是int类型参数,则T为int 类型
add(int, int) //错,不能在函数调用的参数中指定模板形参的类型来直接将类型传入
定义好的函数模板不可以直接使用,只相当于一个模具、规则,可使用不同类型的参数来调用,可减少代码的书写,提高代码复用性。但使用函数模板不会减少最终可执行程序的大小,因为在调用函数模板时,编译器会根据调用时的参数类型进行相应的实例化。
不能直接使用函数模板实现具体操作,必须对模板进行实例化,即将模板参数实例化,就是用具体的类型参数去替换函数模板中的模板参数,生成一个确定的具体类型的真正函数,才能实现运算操作。
函数模板实例化的方法有两种:
(1)隐式实例化 根据函数调用时传入的数据类型,推演出模板形参类型。模板形参的类型是隐式确定的。
比如上面的多种类型相加. 第一次调用add()函数模板:add(12,34),
int add(int t1, int t2)
{
return t1 + t2;
}
编译器生成具体类型函数的过程称为实例化,生成的函数称为模板函数。 3. 生成int类型的函数后,再将实参12和34传入进行运算。
第二次调用add()函数模板:add(12.2, 45.6)
实参为float类型的数据,编译器先将模板实例化为如下模板函数后,再将实 参12.2和45.6传入进行运算:
float add(float t1, float t2)
{
return t1 + t2;
}
可见:每次调用都会根据不同的类型实例化出不同类型的函数,所以最终可执 行程序的大小并不会减少,只是提高了程序员对代码的复用。
存在的问题:隐式实例化不能为同一个模板形参指定两种不同的类型。例如: add(1, 1.2),这样调用,两种形参类型不同,编译器会出错。
解决方法:采用显式实例化。
(2)显式实例化 通过显式声明形式指定模板参数类型。
显式实例化语法格式:
template 函数返回值类型 函数名<实例化的类型>(参数列表);
注意:这是声明语句,要以分号结束,<>中是显式实例的数据类型,即要实例化出一个什么类型的函数。如:显示实例化为int,则在调用时,不是int类型的数据会转换为int类型进行计算。
例如:将例5-1中的add()函数模板显式实例化为int类型
template int add<int>(int t1, int t2);
#include <iostream>
using namespace std;
template<typename T>
T add(T t1, T t2)
{
return t1 + t2;
}
template int add<int>(int t1, int t2);//显示实例化为int类型
int main()
{
cout << add<int>(12, 'A') << endl;//函数模板调用:A-65
cout << add(1.4, 5.7) << endl;//隐式实例化:自动实参推演
cout << add<int>(23.4, 44.2) << endl;//显示声明可省,结果为67
return 0;
}
输出结果
77
7.1
67
上面利用显示实例化指定模板参数是int类型,所以在调用函数时会自动将参 数转换为int类型。比如上面在调用int类型模板函数时,传入一个字符’A’, 则编译器会将字符类型的’A’转换为int类型,然后再与12相加得出结果
注意:对于给定的函数模板实例,显式实例化声明在一个文件中只能出现一次,并且在这个文件中必须给出函数模板的定义,如果定义不可见就会发生错误。
C++编译器也在不断完善,模板实例化的显式声明有时可以省略,只在调用时 用<> 显式指定要实例化的类型也可以。
例如:add(23.4, 44.2)函数调用如果改为add
前面学习过函数的重载。而函数模板可以用来创建一个通用功能的函数,以支持多 种不同形参,不同类型的参数调用,就产生一系列重载函数。
比如定义一个两个数相加的模板
根据传入参数不同,会实例化出不同的函数,比如:
template<typename T>
T add(T t1, T t2)
{
return t1 + t2;
}
根据传入参数不同,会实例化出不同的函数,比如
int add(int t1, int t2) //int类型参数实例化出的函数
{
return t1 + t2;
}
double add(double t1, double t2) //double类型参数实例化出的函数
{
return t1 + t2;
}
最终运行的程序中会有这两个函数,实际上就是重载函数,调用时会根据传入的参数类型来调用相应的函数.
此外,函数模板本身也可以重载,即相同函数模板名可以具有不同的函数模板定义,当进行函数调用时,编译器根据实参的类型与个数来决定调用哪个函数模板来实例化一个函数.
比如:定义一个求两个任意类型数据最大值的函数,还要定义一个求三个任意类型数据最大值的函数,都是求任意类型数据的最大值,可以定义重载的函数模板来实现.
#include <iostream>
using namespace std;
int maxx(int a, int b) //非模板函数,求两个int类型数据的最大者
{
cout << "调用非模板函数" << endl;
return a > b ? a : b;
}
template<typename T>//定义求两个任意类型数据的最大值
T maxx(const T t1, const T t2)
{
cout << "调用两个参数的模板函数" << endl;
return t1 > t2 ? t1 : t2;
}
template<typename T>//定义求三个任意类型数据的最大值
T maxx(T t1, T t2, T t3)
{
cout << "调用三个参数的模板函数" << endl;
return max(max(t1, t2), t3);
}
int main()
{
cout << maxx(1, 2) << endl;//调用非模板函数
//当函数模板和普通函数都符合调用时,优先选择普通函数
cout << maxx(1, 2, 3) << endl;//调用三个参数的函数模板
cout << maxx('a', 'c') << endl;//调用两个参数的函数模板
cout << maxx(6, 3.2) << endl;//调用非模板函数
cout << maxx<>(1, 2) << endl; //若显示使用函数模板,则使用<> 类型列表
cout << maxx(3.0, 4.0) << endl; //如果函数模板产生更好的匹配使用函数模板
cout << maxx(5.0, 6.0, 7.0) << endl; //重载
//cout<<max('a', 100)<<endl; //调用普通函数(报错)
return 0;
}
输出结果
调用非模板函数
2
调用三个参数的模板函数
3
调用两个参数的模板函数
C
调用非模板函数
6
调用两个参数的模板函数
2
调用两个参数的模板函数
4
调用三个参数的模板函数
7
从运行结果可以看出来,当函数模板和普通函数都符合调用时,优先选择普通函数.
但如果函数模板能够更好地实例化一个匹配的函数,则调用时将选择函数模板,比 如上面的maxx(3.0,4.0).
注意:如果有不同类型参数,则只允许使用非模板函数,因为模板是不允许自动类型转化的,但普通函数可以进行自动类型转换.
两者的区别:函数模板不允许自动类型转化,普通函数则能进行自动类型转换
函数模板和普通函数在一起时的调用规则:
(1)函数模板中的每一个类型参数在函数参数表中必须至少使用一次。例如:下 面的函数模板声明是不正确的:函数模板声明了两个参数T1与T2,但在使用时只使用了T1,没使用T2。
template <typename T1,typename T2>
void func(T1 t)
{
//……
}
(2)在全局域中声明的与模板参数同名的对象、函数或类型,在函数模板中将被隐藏。例如:在函数体内访问num是访问的T类型的num,而不是全局变量num
int num;
template <typename T>
void func(T t)
{
T num;
}
(3)函数模板中定义声明的对象或类型不能与模板参数同名。例如:
template <typename T>
void func(T t)
{
Typedef float T; //错误,定义的类型与模板参数名相同
//……
}
(4)模板参数名在同一模板参数表中只能使用一次,但可在多个函数模板声明或定义之间重复使用。例如:
template <typename T, typename T> //错误,在同一个模板中重复定义模板参数
void func1(T t1, T t2) { }
template<typename T>
void func2(T t1) { }
template <typename T> //在不同函数模板中可重复使用相同模板参数名
void func3(T t3) { }
(5)模板的定义和多处声明所使用的模板参数名不一定要必须相同。例如:
//模板的前向声明
template <typename T>
void func1(T t1,T t2);
//模板的定义
template <typename U>
void func1(U t1, U t2)
{
//……
}
(6)函数模板如果有多个模板参数,则每个模板类型前都必须使用关键字typename 或class修饰。例如:
template <typename T, class U> //两个关键字可以混用
void func(T t, U u) { }
template <typename T,U> //错误,每一个模板参数前都要有关键字修饰
void func(T t, U u) { }
除了函数模板外,C++中还支持类模板。类模板是对成员数据类型不同的类的抽象,它说明了类的定义规则,一个类模板可以生成多种具体的类。与函数模板的定义形式类似, 类模板也是使用template关键字和尖括号“<>”中的模板形参进行说明,类的定义形式与普通类相同。
类模板并不能直接使用,需要对其进行实例化,实例化的方法是:类名<具体类型> 对象名。通过类模板实例化得到的对象的操作办法,与普通类对象的操作办法相同。
与函数模板的定义形式类似,类模板也是使用template关键字和尖括号“<>”中 的模板形参进行说明,类的定义形式与普通类相同。格式如下:
template<typename 形参名,typename 形参名…>
class 类名
{
………
}
说明: (1)类模板中的关键字含义与函数模板相同。 (2)类模板中的类型参数可用在类声明和类实现中。类模板的模板形参(类型参数)不能为空,一旦声明了类模板就可以用类模板的形参名声明类中的成员变量和成员函数,即在类中使用内置数据类型的地方都可以使用模板形参名来代替。
例如:
template<typename T> //这里不能有分号。一个模板形参
class A
{
public:
T a; //成员变量
T b; //成员变量
T func(T a, T b); //成员函数声明
};
由于类模板包含类型参数,因此也称为参数化类,如果说类是对象的抽象,对象是类的实例,则类模板是类的抽象,类是类模板的实例。
定义了类模板后就要使用类模板创建对象以及实现类中的成员函数,这个过程其实也是类模板实例化的过程,实例化出的具体类称为模板类。
(1)使用类模板创建对象时,必须指明具体的数据类型。 例如:用上述定义的模板类A创建对象,则在类A后面跟<>,并在里面表明相应的类型:
A<int> a;//类A中凡是用到模板形参的地方都会被int类型所代替
强调:与函数模板不同的是,类模板在实例化时,必须在尖括号中为模板形参显式地指明数据类型(实参),编译器不能根据给定的数据推演出数据类型。即:不存在将整型值10推演为int类型传递给模板形参的实参推演过程,必须要在<>中指定int类型。
(2)当类模板有两个模板形参时,创建对象时,类型之间要用逗号分隔开。
template<typename T1, typename T2>
class B
{
public:
T1 a;
T2 b;
T1 func(T1 a, T2& b);
};
B <int, string> b; //创建模板类B的一个对象
(3)可以使用对象指针的方式来实例化
Point<float, float> *p1 = new Point<float, float>(10.6, 109.3);
Point<char*, char*> *p = new Point<char*, char*>("东京180度", "北纬210度");
注意:赋值号两边都要指明具体的数据类型,且要保持一致。
(4)案例 定义一个类模板实现两个数比较大小(求两数的大者与小者)
#include <iostream>
using namespace std;
template<typename T>
class Compare
{
private:
T t1, t2;
public:
Compare(T a, T b) :t1(a), t2(b) {}
T max() { return t1 > t2 ? t1 : t2; }
T min() { return t1 < t2 ? t1 : t2; }
};
int main()
{
Compare<int> c1(1, 2); //定义int类型的类对象
cout << "int max: " << c1.max() << endl;
Compare<double> c2(1.2, 3.4); //定义double类型的对象
cout << "double min: " << c2.min() << endl;
Compare<char> c3('a', 'b'); // 定义char类型的对象
cout << "char max: " << c3.max() << endl;
return 0;
}
输出结果
int max: 2
double min: 1.2
char max: b
创建对象时,在类名后<>中指定模板形参的类型,编译器先根据类型实例化出一个具体的类,然后再创建这个具体实例的对象。
Compare<int> c1(1, 2)语句,在编译时创建一个类:
class Compare
{
private:
int t1, t2;
public:
Compare(int a, int b) :t1(a), t2(b){}
int max(){ return t1 > t2 ? t1 : t2; }
int min(){ return t1 < t2 ? t1 : t2; }
};
然后再根据这个类创建对象c1。这和函数模板一样,都不会减少最终执行程序的代码。
说明:
如果对函数模板add(T t1, T t2)进行add(1, 1.2)调用,则编译会报错,因为指定了两 种类型的参数,编译器无法确定依据哪个参数来调用。但对于类模板来说,编译器 则不会报错。
template<typename T>
class A
{
public:
A( ) { };
T add(T t1, T t2)
{
return t1+t2;
}
};
A<int> a; //定义对象a
a.add(1,1.2); //不会报错。因为在定义对象a时就已经指定是int类型,当add()函数模板实例化时也会实例化出一个int类型的函数,它会自动将double类型的1.2转换为int类型的1。
(5)模板声明或定义的作用域 模板的声明或定义只能在全局、命名空间或类范围内进行,不能在局部范围、函数 内进行,比如不能在main()函数中声明或定义一个模板。声明或定义一个模板还有 以下几点需注意:
(6)在类模板外部定义成员函数 类中的成员函数既可以在类中定义,也可以在类外定义。类模板中的成员函数同样可以在类模板的定义中定义,也可以在类模板定义之外定义,只是在类外定义成员函数时需要带上模板头:template<模板参数表>。
在类模板外部定义成员函数的方法
template<模板形参表>
函数返回类型 类名<模板形参名>::函数名(参数列表){}
说明:
例如:有下列类模板的定义
template<typename T1, typename T2> //模板头
class B
{
public:
T1 a;
T2 b;
T1 func(T1 a, T2& b);
};
如果在类模板外定义类B的成员函数func(),其实现如下:
template<typename T1, typename T2>
T1 B<T1,T2>::func(T1 a, T2& b) { }
#include <cstdlib>
#include <iostream>
using namespace std;
template<typename T> //类模板
class Array
{
private:
int size;
T* ptr;
public:
Array(T arr[], int s); //构造函数
void show();
};
template<typename T> //类模板外定义其成员函数
Array<T>::Array(T arr[], int s)
{
ptr = new T[s];
size = s;
for (int i = 0; i < size; i++)
{
ptr[i] = arr[i];
}
}
template<typename T> //类模板外定义其成员函数
void Array<T>::show()
{
for (int i = 0; i < size; i++)
cout << *(ptr + i) << " ";
cout << endl;
}
int main()
{
char cArr[] = { 'a', 'b', 'c', 'd', 'e' };
Array<char> a1(cArr, 5); //创建类模板的对象
a1.show();
int iArr[10] = { 1, 2, 3, 4, 5, 6 };
Array<int> a2(iArr, 10);
a2.show();
system("pause");
return 0;
}
输出结果
a b c d e
1 2 3 4 5 6 0 0 0 0
注意:类模板在实例化时,带有模板形参的成员函数并不随着自动被实例化,只有当它被调用或取地址时,才被实例化。
上面创建了两个函数,一个构造函数,一个show函数,主要是在类外定义成员函数的实现方式掌握就行.
(7)类模板实例
template <class T>
//类模板:实现对任意类型数据进行存取
class Store
{ private:
T item; // 用于存放任意类型的数据
int haveValue; // 用于标记item是否已被存入内容
public:
Store(); // 默认形式(无形参)的构造函数
T GetElem(); //提取数据函数
void PutElem(T x); //存入数据函数
};
// 默认形式构造函数的实现
template <class T>
Store<T>::Store(void): haveValue(0) {}
template <class T> // 提取数据函数的实现
T Store<T>::GetElem(void)
{ // 如果试图提取未初始化的数据,则终止程序
if (haveValue == 0)
{ cout << "No item present!" << endl;
exit(1);
}
return item; // 返回item中存放的数据
}
template <class T> // 存入数据函数的实现
void Store<T>::PutElem(T x)
{ haveValue++; // 将haveValue 置为 TRUE,表示item中已存入数值
item = x; // 将x值存入item
}
int main()
{ Student g= {1000, 23};
Store<int> S1, S2;
Store<Student> S3;
Store<double> D;
S1.PutElem(3);
S2.PutElem(-7);
cout << S1.GetElem() << " " << S2.GetElem() << endl;
S3.PutElem(g);
cout << "The student id is " << S3.GetElem().id << endl;
cout << "Retrieving object D " ;
cout << D.GetElem() << endl; //输出对象D的数据成员
// 由于D未经初始化,在执行函数D.GetElement()时出错
}
普通的类中可以声明友元函数和友元类,同样在类模板中也可以声明友元函数和友元类。 类模板中的友元函数可以有三种形式:非模板友元函数、约束模板友元函数、非约束模板友元函数。
非模板友元就是在类模板中声明普通的友元函数和友元类。
例如:在一个类模板中声明一个友元函数
template <typename T>
class A
{
T t;
public:
friend void func();
}
在类模板A中声明了一个普通的友元函数func(),func()函数是类模板A所有实例的友元函数,它可以访问全局对象,也可以使用全局指针访问非全局对象,可以创建自己的对象,也可以访问独立于对象的模板的静态数据成员.
类模板的友元函数也可以拥有模板参数,用于操作类模板对应实例类型的对象.
例如:
template <typename T>
class A
{
T t;
public:
friend void show(const A<T>& a);
}
说明:show()函数并不是模板函数,只是使用一个模板作参数,这就要求在使用友元函数时必须要显式具体化,指明友元函数要引用的参数的类型。
例如:
void show(const A<int>& a); //模板形参为int类型的show()函数是A<int>类的友元函数
void show(const A<double>& a);
//模板形参为double类型的show()函数是A<double>类的友元函数
#include <cstdlib>
#include <iostream>
using namespace std;
template<typename T>
class A
{
T x;
public:
A(const T& t)
{
x = t;
}
friend void show(const A<T>& a);//有参友元函数
};
void show(const A<int>& a)//模板形参为int类型(显示具体化)
{
cout << "int:" << a.x << endl;
}
void show(const A<double>& a)//模板形参为double类型(显示具体化)
{
cout << "double:" << a.x << endl;
}
int main()
{
A<int> a(10);//创建int类型对象
show(a);
A<double> b(12.5);//创建double类型对象
show(b);
return 0;
}
输出结果
int:10
double:12.5
注意:在调用有模板形参的友元函数时,要对友元函数显式具体化,它们是各自相 同类型的对象的友元函数。
这种友元函数本身就是一个函数模板,但其实例化类型取决于类被实例化时的类型 (被约束)。每个类的实例化都会产生一个与之匹配的具体化的友元函数。
(1)在类定义的前面声明函数模板
template<typename T>
void func();
template<typename T>
void show(T& t);
(2)在类模板中将函数模板声明为类的友元函数
template<typename U>
class A
{
……
friend void func<U>(); //友元函数模板
friend void show<>(A<U>& a); //友元函数模板
};
说明:函数名后的<>指出函数模板要实例化的类型,它是由类模板的参数类型决定的。
例如:如果定义一个类模板对象:A
class A
{
……
friend void func<int>();
friend void show<>(A<int>& a);
};
类中友元函数模板会根据类的实例化类型而实例化出与之匹配的具体函数。
注意:类模板的实例化不会实例化一个友元函数,只是声明友元而不实例化, 只有在调用时,函数才会实例化。
上述友元函数声明中,show()函数有类的引用作为参数,可以从函数参数推断 出模板类型参数,所以其函数名后的<>可以为空。
(3)为友元函数模板提供定义
为函数模板提供定义,必须在类内声明,类外定义。
例如:
template<typename T>
void func() { cout << A<T>::成员 << endl; }
template<typename T>
void show(T& t) { cout << t.成员 << endl; }
例如:
#include <cstdlib>
#include <iostream>
using namespace std;
//(1)函数模板声明
template<typename T>
void func();
template<typename T>
void show(T& t);
//类模板定义
template <typename U>
class A
{
private:
U item;
static int count;
public:
A(const U& u) :item(u) { count++; }
~A() { count--; }
//(2)在类模板中将函数模板声明为类的友元函数
friend void func<U>(); //友元函数模板
friend void show<>(A<U>& a); //友元函数模板
};
template<typename T>
int A<T>::count = 0; //类A的T类型对象的个数
//(3)友元函数模板的定义
template<typename T>
void func()
{
cout << "template size: " << sizeof(A<T>) << ";";
cout << " template func(): " << A<T>::count << endl;
}
template<typename T>
void show(T& t)
{
cout << t.item << endl;
}
int main()
{
func<int>(); //调用int类型的函数模板实例,int类型,其大小为 4字节
A<int> a(10); //定义类对象
A<int> b(20);
A<double> c(1.2);
show(a); //调用show()函数,输出类对象的数据成员值
show(b);
show(c);
cout << "func<int> output:\n";
func<int>(); //运行到此,已经创建了两个int类型对象
cout << "func<double>() output:\n";
func<double>();
system("pause");
return 0;
}
输出结果
template size: 4; template func(): 0
10
20
1.2
func<int> output:
template size: 4; template func(): 2
func<double>() output:
template size: 8; template func(): 1
分析:将func()与show()函数定义成了模板并声明为类的友元,在定义函数模 板时是在类外定义的,当调用函数时,func()函数后带有<>说明函数的实例化 类型,而show()是直接调用的。
在类内部声明友元函数模板,友元函数的模板形参与类模板的形参没有联系,此时 友元函数为类模板的非约束模板友元函数。
例如:
template<typename T>
class A
{ ……
template<typename T, typename U> //在类内部声明函数模板
friend void show(T& t, U& u);
};
非约束模板友元函数模板在类内声明,在类外定义;它是类模板每个实例的友元, 可以访问所有实例的类成员。
例如:
#include <cstdlib>
#include <iostream>
using namespace std;
template<typename T>
class A
{
private:
T item;
public:
A(const T& t) :item(t) {}
template<typename U, typename V> //在类内部声明函数模板
friend void show(U& u, V& v);
};
template<typename U, typename V>
void show(U& u, V& v)
{
cout << u.item << "," << v.item << endl;
}
int main()
{
A<int> a(10);
A<int> b(20);
A<double> c(1.2);
cout << "a,b: ";
show(a, b);
cout << "a,c: ";
show(a, c);
system("pause");
return 0;
}
输出结果
a,b: 10,20
a,c: 10,1.2
分析:函数模板的形参类型与类模板的形参类型不相关,因此,它可以接受任何类型的参数:第一次调用传入的是两个int类型的类对象,第二次调用传入的是一个 int类型和一个double类型的对象。
动态多态性是指:基类指针或引用可以指向派生类对象,并调用派生类中重写的函数。
重写/覆盖: 子类中有一个跟父类完全相同的虚函数,子类的虚函数重写了基类的虚函数。
即:子类父类都有这个虚函数 + 子类的虚函数与父类虚函数的 函数名/参数/返回值 都相同 -> 重写/覆盖(注意:参数只看类型是否相同,不看缺省值)
(1)示例1:给一个student的子类对象(临时对象也行),然后把这个对象赋给一个父类指针,通过这个父类指针就可以访问student子类的虚拟函数。
(2)示例2:假设B是子类,A是父类,new一个B类的临时对象,然后把这个临时对象赋给一个父类指针A* p2,通过这个父类指针p2就可以访问子类B的虚拟函数func。

class Person {
public:
Person(const char* name)
:_name(name)
{}
// 虚函数
virtual void BuyTicket()
{
cout << _name << "Person:买票-全价 100¥" << endl;
}
protected:
string _name;
//int _id;
};
class Student : public Person {
public:
Student(const char* name)
:Person(name)
{}
// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
virtual void BuyTicket()
{
cout << _name << " Student:买票-半价 50 ¥" << endl;
}
};
void Pay(Person& ptr)
{
ptr.BuyTicket();
}
int main()
{
string name;
cin >> name;
Student s(name.c_str());
Pay(s);
}
买票场景下的多态 完整代码
买票场景下的多态,完整代码:
普通人 买票时,是全价买票;
学生 买票时,是半价买票;
军人 买票时是优先买票。
class Person {
public:
Person(const char* name)
:_name(name)
{}
// 虚函数
virtual void BuyTicket() { cout << _name << "Person:买票-全价 100¥" << endl; }
protected:
string _name;
//int _id;
};
class Student : public Person {
public:
Student(const char* name)
:Person(name)
{}
// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
virtual void BuyTicket() { cout << _name << " Student:买票-半价 50 ¥" << endl; }
};
class Soldier : public Person {
public:
Soldier(const char* name)
:Person(name)
{}
// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
virtual void BuyTicket() { cout << _name << " Soldier:优先买预留票-88折 88 ¥" << endl; }
};
// 多态两个要求:
// 1、子类虚函数重写的父类虚函数 (重写:三同(函数名/参数/返回值)+虚函数)
// 2、父类指针或者引用去调用虚函数。
//void Pay(Person* ptr)
//{
// ptr->BuyTicket();
//}
void Pay(Person& ptr)
{
ptr.BuyTicket();
}
// 不能构成多态
//void Pay(Person ptr)
//{
// ptr.BuyTicket();
//}
int main()
{
int option = 0;
cout << "=======================================" << endl;
do
{
cout << "请选择身份:";
cout << "1、普通人 2、学生 3、军人" << endl;
cin >> option;
cout << "请输入名字:";
string name;
cin >> name;
switch (option)
{
case 1:
{
Person p(name.c_str());
Pay(p);
break;
}
case 2:
{
Student s(name.c_str());
Pay(s);
break;
}
case 3:
{
Soldier s(name.c_str());
Pay(s);
break;
}
default:
{
cout << "输入错误,请重新输入" << endl;
break;
}
}
cout <<"=======================================" << endl;
} while (option != -1);
return 0;
}
重要技术细节
父类指针或者引用去调用虚函数,传值调用不构成多态。
用子类也不行,必须用父类,比如你用个student,那么你的Person或者Soldier就传不进形参。
void Pay(Person* ptr) //指针调用可以
{
ptr->BuyTicket();
}
void Pay(Person& ptr) //引用调用可以
{
ptr.BuyTicket();
}
// 不能构成多态
//void Pay(Person ptr) //传值调用不可以
//{
// ptr.BuyTicket();
//}
协变(父类与子类虚函数返回值类型不同) 子类重写父类虚函数时,与父类虚函数返回值类型不同,称为协变。
虚函数重写对返回值要求有一个例外:协变,协变是子类虚函数与父类虚函数返回值类型不同,但子类和父类的返回值类型也必须是父子关系指针和引用。
子类虚函数没有写virtual,f依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f() ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 )
ps:我们自己写的时候子类虚函数也写上virtual
class A{};
class B : public A {};
// 虚函数重写对返回值要求有一个例外:协变,父子关系指针和引用
//
class Person {
public:
virtual A* f() {
cout << "virtual A* Person::f()" << endl;
return nullptr;
}
};
class Student : public Person {
public:
// 子类虚函数没有写virtual,f依旧时虚函数,因为先继承了父类函数接口声明
// 重写父类虚函数实现
// ps:我们自己写的时候子类虚函数也写上virtual
// B& f() {
virtual B* f() {
cout << "virtual B* Student::f()" << endl;
return nullptr;
}
};
int main()
{
Person p;
Student s;
Person* ptr = &p;
ptr->f();
ptr = &s;
ptr->f();
return 0;
}
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
所以就有了 子类虚函数没有写virtual,依旧是虚函数;子类虚函数使用的是父类虚函数的缺省参数,只是重写了实现。
多态的坑题目(考接口继承) 子类虚函数没有写virtual,func 依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f() ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 )
ps:我们自己写的时候子类虚函数也写上virtual
class A
{
public:
virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; }
virtual void test(){ func(); }
};
class B : public A
{
public:
void func(int val = 0){ std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B*p1 = new B;
//p1->test(); 这个是多态调用,下有讲解 二->6
p1->func(); //普通调用
A*p2 = new B;
p2->func(); //多态调用
return 0;
}
输出结果:
B->0
B->1
题目讲解:多态调用
.png)
p->test(),调用test中的this指针类型是A*,但指向的是对象B* p中的内容,类B中继承的test函数中又调用func函数,func函数没有写virtual 但依旧是虚函数,只要是虚函数重写就是接口继承,子类先继承了父类函数接口声明(父类接口部分是virtual void func(int va1=1) ),重写是重写父类虚函数的实现部分( 即使用子类的函数的实现部分{}内容 ),所以缺省函数用的是父类的1,实现用的子类的函数实现,打印结果是 B->1
析构函数名统一会被处理成destructor()
何时需要虚析构函数?
当你可能通过基类指针删除派生类对象时
只有派生类 Student 的析构函数重写了 Person 的析构函数,下面的 delete 对象调用析构函数,才能构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。
函数名处理成destructor() 才能满足多态:
如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
注意:期望delete ptr调用析构函数是一个多态调用, 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数.
class Person {
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
// Person析构函数加了virtual,关系就变了
// 重定义(隐藏)关系 -> 重写(覆盖)关系
virtual ~Student() //这里virtual加不加都行
{
cout << "~Student()" << endl;
delete[] _name;
cout << "delete:" << (void*)_name << endl;
}
private:
char* _name = new char[10]{ 'j','a','c','k' };
};
int main()
{
// 对于普通对象是没有影响的
//Person p;
//Student s;
// 期望delete ptr调用析构函数是一个多态调用
// 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
Person* ptr = new Person;
delete ptr; // ptr->destructor() + operator delete(ptr)
ptr = new Student;
delete ptr; // ptr->destructor() + operator delete(ptr)
return 0;
}
输出结果:
~Person()
~Student()
delete:000002D0AB913C70
~Person()
(1)final :修饰虚函数,表示该虚函数不能再被重写;修饰类,该类不能被继承.
(2)override:override写在子类中,要求严格检查是否完成重写,如果没有完成重写就报错.
override的作用时让编译器帮助用户检测是否派生类是否对基类总的某个虚函数进行重写,如 果重写成功,编译通过,否则,编译失败,因此 override作用发生在编译时。
override只能修饰子类的虚函数
override修饰子类成员函数虚函数时,编译时编译器会自动检测是否对基类中那个成员函数进行重写。(在子类里面是可以自己增加 成员函数的,如果这个成员函数不是虚函数,就不可以进行修饰)
示例:如果父类没写virtual能检查出来并报错
(只有重写要求原型相同,原型相同就是指 函数名/参数/返回值都相同)
函数重载:在同一个作用域中,两个函数的函数名相同,参数个数,参数类型,参数顺序至少有一个不同,函数返回值的类型可以相同,也可以不相同。
重定义(也叫做隐藏):是指在继承体系中,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) ,此时子类的函数会屏蔽掉父类的那个同名函数。
重写(也叫做覆盖):是指在继承体系中子类定义了和父类函数名,函数参数,函数返回值完全相同的虚函数。此时构成多态,根据对象去调用对应的函数。
抽象类 -- 在现实一般没有具体对应实体,不能实例化出对象,间接功能:要求子类需要重写,才能实例化出对象。
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象,但可以new别的对象来定义指针,例如Car* pBMW = new BMW;
作用:
注意:
class Car
{
public:
virtual void Drive() = 0;
// // 实现没有价值,因为没有对象会调用他
// /*virtual void Drive() = 0
// {
// cout << " Drive()" << endl;
// }*/
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
动态多态性是通过virtual虚函数实现的,被virtual修饰的成员函数称为虚函数,虚函数的作用是用来实现多态,只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要。
问题举例:
#include <iostream>
using namespace std;
class B0 //基类B0声明
{
public: //外部接口
virtual void display() //虚成员函数
{cout<<"B0::display()"<<endl;}
};
class B1: public B0 //公有派生
{
public:
void display() { cout<<"B1::display()"<<endl; }
};
class D1: public B1 //公有派生
{
public:
void display() { cout<<"D1::display()"<<endl; }
};
void fun(B0 *ptr) //普通函数
{ptr->display();}
int main() //主函数
{ B0 b0,*p; //声明基类对象和指针
B1 b1; //声明派生类对象
D1 d1; //声明派生类对象
p=&b0;
fun(p); //调用基类B0函数成员
p=&b1;
fun(p); //调用派生类B1函数成员
p=&d1;
fun(p); //调用派生类D1函数成员
}
运行结果:
B0::display()
B1::display()
D1::display()
动态多态允许用一个或多个派生类对象的属性配置父类对象。在多态性的支持下,父类对象的某个接口会随着派生类对象的不同而执行不同的操作。
构成动态多态的条件:(a) 调用函数的对象必须是指针或者引用;(b) 被调用的函数必须是虚函数。
后期联编通过虚函数表V-Table实现动态多态,虚函数表是一个实例的虚函数地址表。若某个实例存在虚函数,则该实例的内存中会自动分配虚函数表,指明该实例实际应该调用的函数。实例中通过虚函数指针,指向虚函数表所在的内存位置。
未完待续
目录
流对象与文件操作
提取与插入
常用流类列表

最重要的三个输出流
预先定义的输出流对象
标准输出换向
ofstream fout("b.out");
streambuf* pOld =cout.rdbuf(fout.rdbuf());
//…
cout.rdbuf(pOld);
ofstream myFile("filename");
ofstream myFile; //声明一个静态文件输出流对象
myFile.open("filename"); //打开文件,使流对象与文件建立联系
ofstream myFile("filename", ios_base::out | ios_base::binary);
#include <iostream>
using namespace std;
class Dog
{
public:
void setAge(int a);
void setWeight(float w);
int getAge();
float getWeight();
private:
int age;
float weight;
};
void Dog::setAge(int a)
{
age = a;
}
void Dog::setWeight(float w)
{
weight = w;
}
int Dog::getAge()
{
return age;
}
float Dog::getWeight()
{
return weight;
}
int main()
{
Dog dog;
dog.setAge(5);
dog.setWeight(20.5);
cout << dog.getAge() << dog.getWeight() << endl;
return 0;
}
#include <iostream>
#include <cmath>
using namespace std;
const float PI = 3.1415926;
class Circle
{
public:
Circle(float a, int type);
Circle(const Circle& c);
float getR() { return r; }
float getArea() { return PI*r*r; }
float getPeri() { return 2 * PI*r; }
void setPara(float a, int type);
private:
float r;
};
Circle::Circle(float a, int type)
{
switch (type)
{
case 0: // a为半径
r = a;
break;
case 1: // a为周长
r = a / 2 / PI;
break;
case 2: // a为面积
r = sqrt(a / PI);
break;
}
}
Circle::Circle(const Circle& c)
{
r = c.r;
}
void Circle::setPara(float a, int type)
{
switch (type)
{
case 0: // a为半径
r = a;
break;
case 1: // a为周长
r = a / 2 / PI;
break;
case 2: // a为面积
r = sqrt(a / PI);
break;
}
}
int main()
{
Circle c1(2.0, 0);
cout << c1.getArea() << c1.getPeri() << c1.getR() << endl;
Circle c2(2.0, 1);
cout << c2.getArea() << c2.getPeri() << c2.getR() << endl;
Circle c3(2.0, 2);
cout << c3.getArea() << c3.getPeri() << c3.getR() << endl;
return 0;
}
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
Student(const char n[], int i, int s[]);
Student(const Student &stu);
void outputInfo();
int calcSum();
float calcAve();
private:
char name[20];
int id;
int score[3];
};
Student::Student(const char n[], int i, int s[])
{
int k;
strcpy(name, n);
id = i;
for (k = 0; k < 3; k++)
score[k] = s[k];
}
Student::Student(const Student &stu)
{
int k;
strcpy(name, stu.name);
id = stu.id;
for (k = 0; k < 3; k++)
score[k] = stu.score[k];
}
void Student::outputInfo()
{
cout << "name: " << name << endl;
cout << "id: " << id << endl;
cout << "c++ score: " << score[0] << endl;
cout << "eng score: " << score[1] << endl;
cout << "math score: " << score[2] << endl;
}
int Student::calcSum()
{
int i, sum=0;
for (i = 0; i < 3; i++)
sum += score[i];
return sum;
}
float Student::calcAve()
{
return calcSum() / 3.0;
}
int main()
{
int score[3] = { 90,90,80 };
Student stu1("ZhangSan", 1, score);
stu1.outputInfo();
cout << "sum: " << stu1.calcSum() << endl;
cout << "ave: " << stu1.calcAve() << endl;
return 0;
}
#include <iostream>
using namespace std;
class Rect
{
public:
Rect(double l, double w);
void calculateArea();
private:
double len;
double width;
};
Rect::Rect(double l, double w)
{
len = l;
width = w;
}
void Rect::calculateArea()
{
cout << "面积为:" << len * width << endl;
}
int main()
{
Rect r(3, 4);
r.calculateArea();
return 0;
}
其中,第0个元素的左邻元素为最后一个元素。最后一个元素的右邻元素为第0个元素。例如:假设原数组为a[10],新数组为b[10],则b[1] = (a[0]+a[1]+a[2])/3,b[0]=(a[9]+a[0]+a[1])/3,b[9]=(a[8]+a[9]+a[0])/3。
具体要求如下:
私有数据成员
float a[10], b[10](数组大小固定为10)
公有成员函数
构造函数
void process(),实现上述处理
void print(),在屏幕上打印数组a和b
在主程序中测试上述功能。
#include<iostream>
using namespace std;
class ARRAY {
float a[10], b[10];
public:
ARRAY(float t[])
{
for (int i = 0; i<10; i++)
a[i] = t[i];
}
void process();
void print()
{
int i;
for (i = 0; i<10; i++)
{
if (i % 10 == 5)
cout << endl;
cout << a[i] << '\t';
}
cout << endl;
for (i = 0; i<10; i++) {
if (i % 10 == 5)
cout << endl;
cout << b[i] << '\t';
}
cout << endl;
}
};
void ARRAY::process()
{
int i, j, k;
for (i = 0; i<10; i++)
{
j = i - 1;
k = i + 1;
if (j<0)
j = 10 + j;
if (k>9)
k = 10 - k;
b[i] = (a[j] + a[i] + a[k]) / 3;
}
}
int main() {
float aa[10];
for (int k = 0; k<10; k++)
aa[k] = (float)k * 3;
ARRAY arr(aa);
arr.process();
arr.print();
}
#include<iostream>
using namespace std;
class Point
{
public:
Point()
{
m_x = 0;
m_y = 0;
}
Point(double x, double y)
{
m_x = x;
m_y = y;
cout << "Constructor1 was called." << endl;
}
Point(Point &);
double getX()
{
return m_x;
}
double getY()
{
return m_y;
}
private:
double m_x, m_y;
};
Point::Point(Point &p)
{
m_x = p.m_x;
m_y = p.m_y;
cout << "Copy Constructor1 was called." << endl;
}
class Rect
{
public:
Rect(Point a, double l, double w);
Rect(Rect &);
double getm_S();
double getm_L();
private:
Point p1;
double len, width;
double m_S, m_L;
};
Rect::Rect(Point a, double l, double w) :p1(a)
{
len = l;
width = w;
cout << "右上角顶点坐标为:" << "(" << (p1.getX() - len) << "," << p1.getY() << ")" << endl;
cout << "右下角顶点坐标为:" << "(" << (p1.getX() - len) << "," << (p1.getY() - width) << ")" << endl;
cout << "左上角顶点坐标为:" << "(" << p1.getX() << "," << (p1.getY() - width) << ")" << endl;
}
Rect::Rect(Rect &p) :p1(p.p1)
{
len = p.len;
width = p.width;
cout << "Copy Constructor2 was called." << endl;
}
double Rect::getm_L()
{
return len * 2 + width * 2;
}
double Rect::getm_S()
{
return len*width;
}
int main()
{
Point s(3, 4);
Rect ss(s, 1, 2);
cout << "矩形的面积为:" << ss.getm_S() << endl;
cout << "矩形的周长为:" << ss.getm_L() << endl;
return 0;
}
#include<iostream>
using namespace std;
class Ear
{
public:
Ear(int c)
{
color = c;
cout << "调用Ear类的构造函数." << endl;
}
Ear(Ear &E);
~Ear()
{
cout << "调用Ear类的析构函数." << endl;
}
int getear()
{
return color;
}
private:
int color;
};
Ear::Ear(Ear &E)
{
color = E.color;
cout << "调用Ear类的复制构造函数." << endl;
}
class Dog
{
public:
Dog(Ear, Ear, double, int);
Dog(Dog &D);
~Dog()
{
cout << "调用Dog类的析构函数." << endl;
}
int getleftear()
{
return m_leftear.getear();
}
int getrightear()
{
return m_rightear.getear();
}
double getweight()
{
return m_weight;
}
int getage()
{
return m_age;
}
private:
Ear m_leftear, m_rightear;
double m_weight;
int m_age;
};
Dog::Dog(Ear leftear, Ear rightear, double weight, int age) :m_leftear(leftear), m_rightear(rightear)
{
m_weight = weight;
m_age = age;
cout << "调用Dog类的构造函数." << endl;
}
Dog::Dog(Dog &D) :m_leftear(D.m_leftear), m_rightear(D.m_rightear)
{
cout << "调用Dog类的复制构造函数." << endl;
}
int main()
{
Ear c1(1);
Ear c2(2);
Dog dog(c1, c2, 15, 7);
cout << "狗的左耳的颜色:" << dog.getleftear() << endl;
cout << "狗的右耳的颜色:" << dog.getrightear() << endl;
cout << "狗的体重:" << dog.getweight() << endl;
cout << "狗的年龄:" << dog.getage() << endl;
return 0;
}
#include<iostream>
using namespace std;
class Book
{
public:
Book(double p, int n);
Book(Book &co);
~Book() { Bookcount--; cout << "the destructor was called." << endl; }
static void showcount();
private:
double price;
int bookid;
static int Bookcount;
};
Book::Book(double p, int n)
{
price = p;
bookid = n;
Bookcount++;
cout << "the constructor was called." << endl;
}
Book::Book(Book &co)
{
price = co.price;
bookid = co.bookid;
Bookcount++;
cout << "the copy constructor was called." << endl;
}
void Book::showcount()
{
cout << "书本的总数:" << Bookcount << "本" << endl;
}
int Book::Bookcount = 0;
int main()
{
Book::showcount();
Book b1(45.0, 001);
Book b2(53.0, 002);
Book b3(52.0, 003);
Book b4(b3);
b3.showcount();
Book::showcount();
return 0;
}
#include <cmath>
#include <iostream>
using namespace std;
class Point{
public:
Point(){}
Point(double x, double y);
Point(const Point &p);
~Point(){}
double getX();
double getY();
void printpoint();
private:
double x;
double y;
};
Point::Point(double x , double y )
{
this->x = x;
this->y = y;
//cout<<"creat point"<<endl;
}
Point::Point(const Point &p)
{
this->x = p.x;
this->y = p.y;
//cout<<"copy point"<<endl
}
double Point::getX()
{
return x;
}
double Point::getY()
{
return y;
}
void Point::printpoint()
{
cout<<"("<<x<<" , "<<y<<")";
}
class Triangle{
public:
Triangle(Point p1, Point p2, Point p3);
Triangle(double x1, double y1, double x2, double y2, double x3, double y3);
Triangle(const Triangle &t);
~Triangle(){}
double callen(Point p1, Point p2);
double getArea();
double getperimeter();
void printperimeter();
void printlength();
private:
Point p1, p2, p3;
double perimeter;
void calperimeter();
};
Triangle::Triangle(Point np1, Point np2, Point np3):p1(np1),p2(np2),p3(np3)
{
calperimeter();
}
Triangle::Triangle(double x1, double y1, double x2, double y2, double x3, double y3):p1(x1,y1),p2(x2,y2),p3(x3,y3)
{
calperimeter();
}
Triangle::Triangle(const Triangle &t):p1(t.p1),p2(t.p2),p3(t.p3),perimeter(t.perimeter){}
double Triangle::getperimeter()
{
return perimeter;
}
double Triangle::callen(Point p1, Point p2)
{
double len;
double x = p1.getX() - p2.getX();
double y = p1.getY() - p2.getY();
len = sqrt(x*x + y*y);
return len;
}
void Triangle::calperimeter()
{
if((callen(p1,p2) + callen(p2,p3)) > callen(p1,p3) && abs(callen(p1,p2)-callen(p2,p3)) < callen(p1,p3))
perimeter = callen(p1,p2) + callen(p2,p3) + callen(p1,p3);
else
{
cout<<"this is not a triangle!"<<endl;
perimeter = 0;
}
}
double Triangle::getArea()
{
double p = perimeter/2;
return sqrt(p*(p - callen(p1,p2))*(p - callen(p2,p3)) * (p - callen(p1,p3)));
}
void Triangle::printperimeter()
{
cout<<"perimeter: "<<perimeter<<endl;
}
void Triangle::printlength()
{
cout<<"length of three side of the triangle: "<<callen(p1,p2)<<" "<<callen(p2,p3)<<" "<<callen(p1,p3)<<endl;
}
void test(double x1, double y1, double x2, double y2, double x3, double y3)
{
Point p1(x1,y1),p2(x2,y2),p3(x3,y3);
Triangle t(p1,p2,p3);
t.printlength();
t.printperimeter();
cout<<"area: "<<t.getArea()<<endl;
}
int main()
{
test(1,1,5,2,8,0);
test(1,1,5,2,9,3);
test(0,0,1,0,0,1);
}
两个功能写在同一个程序中。
#include <iostream>
using namespace std;
class GBank;
class BBank;
class CBank
{
public:
CBank(float b) { balance = b; }
friend float totalBalanceFriend(CBank &c, BBank &b, GBank &g);
float getBalance() { return balance; }
private:
float balance;
};
class BBank
{
public:
BBank(float b) { balance = b; }
friend float totalBalanceFriend(CBank &c, BBank &b, GBank &g);
float getBalance() { return balance; }
private:
float balance;
};
class GBank
{
public:
GBank(float b) { balance = b; }
friend float totalBalanceFriend(CBank &c, BBank &b, GBank &g);
float getBalance() { return balance; }
private:
float balance;
};
float totalBalanceFriend(CBank &c, BBank &b, GBank &g)
{
return c.balance + b.balance + g.balance;
}
int main()
{
CBank c(10);
BBank b(20);
GBank g(30);
cout << totalBalanceFriend(c, b, g) << endl;
cout << c.getBalance() + b.getBalance() + g.getBalance() << endl;
return 0;
}
#include <iostream>
using namespace std;
class Date
{
public:
Date(int y, int m, int d)
{
year = y;
month = m;
day = d;
}
Date(Date &date)
{
year = date.year;
month = date.month;
day = date.day;
}
private:
int year, month, day;
};
class Project
{
public:
Project(Date &date1, Date &date2, string &c, int i);
Project(Project &project);
~Project() { count--; }
static void showCount() { cout << count << endl; }
private:
Date startDate, endDate;
string content;
int isFinished;
static int count;
};
int Project::count = 0;
Project::Project(Date &date1, Date &date2, string &c, int i): startDate(date1), endDate(date2), content(c)
{
isFinished = i;
count++;
}
Project::Project(Project &project): startDate(project.startDate), endDate(project.endDate), content(project.content)
{
isFinished = project.isFinished;
count++;
}
int main()
{
string str = "tiyuguan";
Date date1(2000, 1, 1), date2(2000, 12, 30);
Project p1(date1, date2, str, 0);
Project p2(p1);
Project::showCount();
return 0;
}
#include <iostream>
using namespace std;
class Monitor
{
public:
void incident();
static void print();
private:
static int count;
};
int Monitor::count = 0;
void Monitor::incident()
{
count++;
}
void Monitor::print()
{
cout << count << endl;
}
int main()
{
Monitor marr[3];
marr[0].incident();
marr[1].incident();
Monitor::print();、
Monitor *pmarr = new Monitor[3];
pmarr[0].incident();
pmarr[1].incident();
Monitor::print();
Monitor *pm = new Monitor;
pm->incident();
Monitor::print();
delete []pmarr;
delete pm;
return 0;
}
#include <iostream>
using namespace std;
class Point
{
public:
Point(int x=0, int y=0)
{
this->x = x;
this->y = y;
}
void Move(int x, int y)
{
this->x = x;
this->y = y;
}
int getX() { return x; }
int getY() { return y; }
private:
int x, y;
};
int main()
{
int i;
Point *p = new Point[10];
for (i = 0; i < 10; i++)
p[i].Move(i, i)l
for (i = 0; i < 10; i++)
cout << "第" << i << "个元素坐标为(" << p[i].getX(
<< "," << p[i].getY() << ")" << endl;
return 0;
}
#include <iostream>
using namespace std;
class Date
{
private:
int year, month, day;
public:
Date(int y = 2000, int m = 1, int d = 1);
void showDate()
{
cout << "The date is " << this->year << "." << this->month << "." << this->day << ".\n";
}
};
Date::Date(int y, int m, int d)
{
this->year = y;
this->month = m;
this->day = d;
}
int main()
{
Date d1(2000, 1, 1);
Date da1[2];
Date *da = new Date;
Date *Da = new Date[3];
d1.showDate();
da->showDate();
for (int i = 0; i<2; i++) {
da1[i].showDate();
}
for (int i = 0; i<3; i++) {
Da[i].showDate();
}
if (da != NULL) {
delete da;
}
if (Da != NULL) {
delete[]Da;
}
}
#include <iostream>
using namespace std;
class Student
{
public:
Student(char n[] = NULL, int i = 0, float s[] = NULL);
void setName(const char n[]) { strcpy(name,n); }
void setID(int i) { id = i; };
void setScores(float s[]);
void showScores()
{
cout << "score1=" << scores[0] << endl
<< "score2=" << scores[1] << endl
<< "score3=" << scores[2] << endl;
}
private:
char name[20];
int id;
float scores[3];
};
Student::Student(char n[], int i, float s[])
{
if (n != NULL)
strcpy(name, n);
id = i;
int k;
if(s!= NULL)
for (k = 0; k < 3; k++)
scores[k] = s[k];
}
void Student::setScores(float s[])
{
int k;
for (k = 0; k < 3; k++)
scores[k] = s[k];
}
class Banji
{
public:
Banji(int n);
Banji(const Banji &b);
~Banji();
Student &callStudent(int i);
private:
Student *pstu;
int numberOfStudent;
};
Banji::Banji(int n)
{
pstu = new Student[n];
numberOfStudent = n;
}
Banji::Banji(const Banji &b)
{
int i;
numberOfStudent = b.numberOfStudent;
pstu = new Student[numberOfStudent];
for (i = 0; i < numberOfStudent; i++)
pstu[i] = b.pstu[i];
}
Banji::~Banji()
{
delete[]pstu;
}
Student &Banji::callStudent(int i)
{
return pstu[i];
}
int main()
{
Banji b1(50);
Banji b2(b1);
float score[3] = { 100,90,80 };
b1.callStudent(10).setName("Zhangsan");
b1.callStudent(10).setID(1);
b1.callStudent(10).setScores(score);
b1.callStudent(10).showScores();
return 0;
}
#include <iostream>
using namespace std;
class Student
{
public:
Student(char n[] = NULL, int i = 0, float s[] = NULL);
void setName(const char n[]) { strcpy(name, n); }
void setID(int i) { id = i; };
void setScores(float s[]);
void showScores()
{
cout << "score1=" << scores[0] << endl
<< "score2=" << scores[1] << endl
<< "score3=" << scores[2] << endl;
}
private:
char name[20];
int id;
float scores[3];
};
Student::Student(char n[], int i, float s[])
{
if (n != NULL)
strcpy(name, n);
id = i;
int k;
if (s != NULL)
for (k = 0; k < 3; k++)
scores[k] = s[k];
}
void Student::setScores(float s[])
{
int k;
for (k = 0; k < 3; k++)
scores[k] = s[k];
}
int main()
{
float score[3] = { 100,90,80 };
Student *pstu = new Student[10];
pstu[5].setName("Test");
pstu[5].setID(5);
pstu[5].setScores(score);
pstu[5].showScores();
delete[]pstu;
return 0;
}
#include<iostream>
using namespace std;
class Object
{
public:
Object();
Object(int weight);
Object(Object &obj);
~Object();
int getweight() { return weight; }
private:
int weight;
};
Object::Object()
{
weight = 0;
cout << "O. initialization is calling" << endl;
}
Object::Object(int weight)
{
this->weight = weight;
cout << "O. constructed is calling" << endl;\
}
Object::Object(Object &obj)
{
weight = obj.weight;
cout << "O. copy constructed is calling" << endl;
}
Object::~Object()
{
cout << "O. destructed is calling" << endl;
}
class Box : public Object
{
public:
Box();
Box(int height, int weight, int);
Box(Box &box);
~Box();
void showbox();
private:
int height;
int weight;
};
Box::Box()
{
height = 0;
weight = 0;
cout << "B. initialization is calling" << endl;
}
Box::Box(int height, int weight, int Oweight) :Object(Oweight)
{
this->weight = weight;
this->height = height;
cout << "B. constructed is calling" << endl;
}
Box::Box(Box &box) :Object(box)
{
weight = box.weight;
height = box.height;
cout << "B. copy constructed is calling" << endl;
}
Box::~Box()
{
cout << "B. destructed is calling" << endl;
}
void Box::showbox()
{
cout << "the object's weight:" << getweight() << endl;
cout << "the box's height:" << height << endl;
cout << "the box's weight:" << weight << endl;
}
int main()
{
Box box(12, 13, 11);
box.showbox();
return 0;
}
#include<iostream>
#include<string>
using namespace std;
class Mammal
{
public:
Mammal();
Mammal(string name, int age);
void speak()
{
cout << "动物叫。" << endl;、
}
protected:
string m_name;
int m_age;
};
Mammal::Mammal(string name, int age) :m_name(name)
{
m_age = age;
}
Mammal::Mammal() : m_name()
{
m_age = 0;
}
class Dog :public Mammal
{
public:
Dog(string name, int age);
void speak()
{
cout << "狗叫。" << endl;
}
};
Dog::Dog(string name, int age) :Mammal(name, age){}
class Cat :public Mammal
{
public:
Cat(string name, int age);
void speak()
{
cout << "猫叫。" << endl;
}
};
Cat::Cat(string name, int age) :Mammal(name, age){}
int main()
{
Dog dog("Dog", 12);
dog.speak();
Cat cat("Cat", 10);
cat.speak();
dog.Mammal::speak();
cat.Mammal::speak();
return 0;
}
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person(string n, int a, int g)
{
name = n;
age = a;
gender = g;
}
void display()
{
cout << "name:" << name << endl
<< "age:" << age << endl
<< "gender:" << gender << endl;
}
string getName() { return name; }
int getAge() { return age; }
int getGender() { return gender; }
private:
string name;
int age;
int gender;
};
class Teacher : public Person
{
public:
Teacher(string n, int a, int g, string t);
void display()
{
cout << "name:" << getName() << endl
<< "age:" << getAge() << endl
<< "gender:" << getGender() << endl
<< "title:" << title << endl;
}
private:
string title;
};
Teacher::Teacher(string n, int a, int g, string t):Person(n, a, g), title(t){}
// Student略
int main()
{
Person p("PQ", 35, 0);
p.display();
Teacher t("PQ", 35, 0, "AP");
t.display();
return 0;
}
#include<iostream>
using namespace std;
class Vehicle//声明汽车类
{
private:
int wheels;//车轮
protected:
double weight;//车重
public:
int getWheels()
{
return wheels;
}
void setWheels(int wh)
{
wheels = wh;
}
int getWeight()
{
return weight;
}
void setWeight(int w)
{
weight = w;
}
};
class Car:private Vehicle//声明小车类,私有继承汽车类
{
private:
int displacement;
public:
void set(int dis, int w, int wh)
{
displacement = 100;
weight = w;
setWheels(wh);
}
void show()
{
cout<<"I'm car, I have "<<getWheels()<<" wheels :"<<weight<<" "<<displacement<<endl;
}
};
class Truck:protected Vehicle//声明卡车类,私有继承汽车类
{
private:
int payload;
public:
void set(int pay, int w, int wh)
{
payload = pay;
weight = w;
setWheels(wh);
}
void show()
{
cout<<"I'm truck, I have "<<getWheels()<<" wheels :"<<weight<<" "<<payload<<endl;
}
};
class Van:public Vehicle//声明卡车类,私有继承汽车类
{
private:
int passager_load;
public:
void set(int pass, int w, int wh)
{
passager_load = pass;
weight = w;
setWheels(wh);
}
void show()
{
cout<<"I'm van, I have "<<getWheels()<<" wheels :"<<weight<<" "<<passager_load<<endl;
}
};
int main()
{
Car C;
Truck T;
Van V;
C.set(100,100,4);
C.show();
T.set(200,200,6);
T.show();
V.set(6,300,6);
V.show();
return 0;
}
#include<iostream>
using namespace std;
const double PI = 3.14;
class Point
{
public:
Point(double xx, double yy); //constructor
void Display(); //display point
private:
double x, y; //平面的点坐标x,y
};
Point::Point(double xx, double yy) //constructor
{
x = xx;
y = yy;
}
void Point::Display()
{
cout << "Point(" << x << "," << y << ")" << endl;
}
class Circle :public Point
{
public:
Circle(double xx, double yy, double rr);
double Area();
double Perimeter();
void Display();
private:
double r;
};
Circle::Circle(double xx, double yy, double rr) :Point(xx, yy)
{
r = rr;
}
double Circle::Area()
{
return PI*r*r;
}
double Circle::Perimeter()
{
return 2 * PI*r;
}
void Circle::Display()
{
cout << "Center:";
Point::Display();
cout << "Radius:" << r << endl;
cout << "Area:" << Area() << endl;
cout << "Perimeter:" << Perimeter() << endl;
}
int main()
{
double x, y, r;
cin >> x >> y >> r; //圆心的点坐标及圆的半径
Circle C(x, y, r);
C.Display(); //输出圆心点坐标,圆的半径,圆的面积,圆的周长
return 0;
}
#include<iostream>
#include<cmath>
using namespace std;
class Clock
{
public:
Clock(int hour = 0, int minute = 0, int second = 0)
{
this->hour = hour;
this->minute = minute;
this->second = second;
}
Clock operator+(Clock &c1);
Clock operator-(Clock &c1);
Clock &operator+=(Clock &c1);
Clock &operator-=(Clock &c1);
friend ostream &operator << (ostream &out, Clock &c)
{
out << c.hour << "时" << c.minute << "分" << c.second << "秒";
return out;
}
private:
int hour;
int minute;
int second;
};
Clock Clock::operator+(Clock &c1)
{
int s = this->second + c1.second;
int h = this->hour + c1.hour;
int m = this->minute + c1.minute;
if (s>60)
{
s = s % 60;
m++;
}
if (m>60)
{
m = m % 60;
h++;
}
return Clock(h, m, s);
}
Clock Clock::operator-(Clock &c1)
{
int h = this->hour - c1.hour;
int m = this->minute - c1.minute;
int s = this->second - c1.second;
if (s<0)
{
s = s + 60;
m--;
}
if (m<0)
{
m = m + 60;
h--;
}
return Clock(h, m, s);
}
Clock &Clock::operator+=(Clock &c1)
{
this->hour += c1.hour;
this->minute += c1.minute;
this->second += c1.second;
if ((this->second)>60)
{
(this->second) %= 60;
(this->minute)++;
}
if ((this->minute)>60)
{
(this->minute) %= 60;
(this->hour)++;
}
return *this;
}
Clock &Clock::operator-=(Clock &c1)
{
this->hour -= c1.hour;
this->minute -= c1.minute;
this->second -= c1.second;
if ((this->second)<0)
{
(this->second) += 60;
(this->minute)--;
}
if ((this->minute)<0)
{
(this->minute) += 60;
(this->hour)--;
}
return *this;
}
int main()
{
Clock c1(21, 50, 25), c2(23, 23, 50), c3, c4;
cout << c1 << endl;
cout << c2 << endl;
c3 = c1 + c2;
c4 = c2 - c1;
cout << c1 << " + " << c2 << " = " << c3 << endl;
cout << c2 << " - " << c1 << " = " << c4 << endl;
c2 += c1;
cout << c1 << endl;
cout << c2 << endl;
c2 -= c1;
cout << c1 << endl;
cout << c2 << endl;
return 0;
}